Compare commits

...

116 commits

Author SHA1 Message Date
Morgan McMillian 811b9ab0b3 Updated README 2021-05-01 07:11:50 -07:00
Morgan McMillian 0173d8c396 updated makefile
set version number
2019-05-19 08:16:33 -07:00
Morgan McMillian fd2e8e52e8 added icons for ios 2019-05-19 08:15:49 -07:00
Morgan McMillian fd3237fff1 remove cordova platforms 2019-05-19 06:32:30 -07:00
Morgan McMillian 587531ab6e fix paste from clipboard into login resolves #83 2019-05-19 06:23:38 -07:00
Morgan McMillian 6d79b9b600 replace sample auth file 2019-05-18 16:28:05 -07:00
Morgan McMillian 4f8768966c replace login 2019-05-18 16:23:00 -07:00
Morgan McMillian d464479b4f version bump for release 2018-11-21 16:21:34 -08:00
Morgan McMillian eb158ed5b3 added copy post link to clipboard as a way to work around #82 while browser bugs are fixed 2018-11-21 15:46:22 -08:00
Morgan McMillian fc1849e384 update to changelone 2018-11-21 15:38:03 -08:00
Morgan McMillian ea641c52fe extract links in posts in include in the share or clipboard data resolves issue #68 2018-11-21 09:50:40 -08:00
Morgan McMillian 1e0d8a2af0 add clipboard support and limit share option to android devices 2018-11-21 07:42:18 -08:00
Morgan McMillian 7177f63b27 blarp 2018-11-21 06:40:10 -08:00
Morgan McMillian 3ac8ad780f fix up thumbnail rendering 2018-11-21 06:39:22 -08:00
Morgan McMillian 4261fcda94 move self profile lookup to when profile page is selected 2018-11-20 15:15:59 -08:00
Morgan McMillian 724db2aafd remove conflicting components module 2018-11-20 15:15:20 -08:00
Morgan McMillian 9e540f774b add user profile to main menu 2018-11-20 14:38:29 -08:00
Morgan McMillian da9a3a801a add user interface definition from dale 2018-11-20 14:38:01 -08:00
Morgan McMillian c224b081fa fix blocked/muted status 2018-11-18 20:35:05 -08:00
Morgan McMillian eb8fbd59ca fix menu definition 2018-11-18 20:34:46 -08:00
Morgan McMillian 4e8f7a4c69 add icons to main menu 2018-11-18 19:59:39 -08:00
Morgan McMillian 6647a6ccd0 add profile menu with mute and block functions 2018-11-18 19:59:28 -08:00
Morgan McMillian 73664e4f44 add user following and followers page 2018-11-18 16:39:53 -08:00
Morgan McMillian 38f8849972 add posts and bookmarks to profile page 2018-11-17 22:34:32 -08:00
Morgan McMillian bc995eb707 finish post component split from stream and thread pages 2018-11-17 16:59:00 -08:00
Morgan McMillian 7bb6faf97a Merge branch 'post-component' into 0.7.0 2018-11-16 17:55:32 -08:00
Morgan McMillian b4b3ddeddb version bump for testing 2018-11-16 17:54:45 -08:00
Morgan McMillian 724f5dbb29 android build cleanup 2018-11-16 17:49:59 -08:00
Morgan McMillian e862382d38 restore _system target for opening links 2018-11-16 17:32:59 -08:00
Morgan McMillian 257603ec95 Change alignment of buttons and temporarily disable 2018-11-16 17:16:33 -08:00
Morgan McMillian c41a422968 start building post component 2018-11-13 18:06:44 -08:00
Morgan McMillian eaefe3908b adjust wording on about page 2018-11-13 18:04:28 -08:00
Morgan McMillian d94bec3a4b Add Profile page issue #32 2018-11-12 16:34:38 -08:00
Morgan McMillian 7f04d55641 change target for links in posts 2018-11-12 13:37:09 -08:00
Morgan McMillian d80cdf1690 Change target for opening links 2018-11-12 13:27:17 -08:00
Morgan McMillian 1da3eb3b98 Added about page 2018-11-12 13:04:31 -08:00
Morgan McMillian 77db17acd4 Setting to hide images in timeline resolves #75 2018-11-11 07:54:18 -08:00
Morgan McMillian 198b804f2e additional pwa testing bits 2018-11-11 07:53:28 -08:00
Morgan McMillian abd237fc64 fix clean target 2018-11-10 07:36:55 -08:00
Morgan McMillian dad8eed809 Update to Cordova Android 7.1.2 2018-11-10 07:03:05 -08:00
Morgan McMillian 9a02daa626 Add missing cordova-sqlite-storage plugin resolves issue #84 2018-11-09 15:43:41 -08:00
Morgan McMillian 7c044cf2b8 updated build settings for ubuntu touch issue #76 2018-11-01 06:22:33 -07:00
Morgan McMillian a5064ee109 enable service worker 2018-10-29 17:21:22 -07:00
Morgan McMillian e3d62e27d3 fix app title in index and manifest and updating icon sizes 2018-10-29 17:20:39 -07:00
Morgan McMillian 5a0a18ab95 Fix reference to new-post from thread page
Remove more unused imports from stream and post-menu
2018-10-07 08:00:09 -07:00
Morgan McMillian 126f00e165 Added missing pnut-butter import and removed unused imports 2018-10-07 07:39:40 -07:00
Morgan McMillian f34216213f Split out new-post modal and post-menu template and code 2018-10-07 07:36:07 -07:00
Morgan McMillian 347ba44bd1 Add target for ubuntu touch in makefile 2018-09-03 08:47:35 -07:00
Morgan McMillian 0565ce9aee add build for ubuntu touch #76 2018-09-03 08:18:32 -07:00
Morgan McMillian 93e4c38c71 update changelog and bump version for release 2018-09-03 07:39:59 -07:00
Morgan McMillian 03a6e19b5d remove thumbnail tag so image scales properly to be visible in the post issue #73 2018-09-02 18:23:11 -07:00
Morgan McMillian 3b13d7e5ba updated readme resolves issue #71 2018-09-02 17:54:10 -07:00
Morgan McMillian 284836e0dd show thumbnail by default in timeline but open in browser when clicked. issue #73 and #16 2018-09-02 17:49:21 -07:00
Morgan McMillian ae18ab36cf tweak ignore file 2018-09-02 17:12:55 -07:00
Morgan McMillian 6cf54b32ac remove cordova-plugin-statusbar plugin #72 2018-09-02 17:12:41 -07:00
Morgan McMillian c25177f758 remove cordova-plugin-console and ionic-plugin-keyboard plugins issue #72 2018-09-02 16:55:16 -07:00
Morgan McMillian 10bc57a281 update cordova-plugin-filepath to latest version 2018-09-02 12:03:39 -07:00
Morgan McMillian 1c970a50e3 ingoring the plugins tree as it is not needed for f-droid builds 2018-07-18 21:38:52 -07:00
Morgan McMillian afa50236e2 remove the plugin tree for testing 2018-07-18 21:22:02 -07:00
Morgan McMillian 892cb0e141 add native android build sources 2018-07-15 19:24:48 -07:00
Morgan McMillian 79aee37688 add target for preparing the native android build tree 2018-07-15 14:50:16 -07:00
Morgan McMillian 749281d959 add action to open post in browser resolves issue #65 2018-07-07 08:16:41 -07:00
Morgan McMillian 43a8e0c43b update dependencies 2018-07-07 07:46:18 -07:00
Morgan McMillian c889a7271a update to use any interface for debugging 2018-07-07 07:39:22 -07:00
Morgan McMillian dbc7b94130 version bump for 6.2 2018-05-07 12:49:26 -07:00
Morgan McMillian 7d07f5c117 adjust targets to align with cordova update 2018-05-07 12:48:20 -07:00
Morgan McMillian f146597825 update to cordova android platform 2018-05-07 10:24:01 -07:00
Morgan McMillian ec4dbf0265 added makefile 2018-05-06 18:32:29 -07:00
Morgan McMillian d5ed4b9668 Remove BB10 since it's not supported and clarify details around
setup and build issue #54
2018-05-06 06:42:47 -07:00
Morgan McMillian 53ffd17808 work around for missing status bar resolves #63 2018-05-05 17:12:43 -07:00
Morgan McMillian cc87515a01 add .sourcemaps to ignore 2018-05-05 17:11:38 -07:00
Morgan McMillian 8ba0128196 updated cordova dependencies and ionic cli 2018-05-05 16:47:19 -07:00
Morgan McMillian a65f944ff7 version bump for release 2018-02-24 17:43:08 -08:00
Morgan McMillian 2cfcf649de remove &simple_login=1 from auth url resolves #56 2018-02-24 17:42:16 -08:00
Morgan McMillian 06079a4944 Added a changelong *finally* 2018-02-24 08:45:15 -08:00
Morgan McMillian 3ab9d62276 pull debug bit 2018-02-24 07:37:47 -08:00
Morgan McMillian 8f3e5fe440 version bump 2018-02-24 07:22:33 -08:00
Morgan McMillian e5cdc80b64 add progress bar for file uploads resolves #53 2018-02-24 07:22:13 -08:00
Morgan McMillian 305b570cfa added missing device module 2018-02-22 13:23:18 -08:00
Morgan McMillian b5c68027c4 sync config 2018-02-20 20:59:30 -08:00
Morgan McMillian 1c9dafa4e3 handle change in scope 2018-02-20 20:42:42 -08:00
Morgan McMillian e55fafe04b handle invalid auth token resolve #52 2018-02-19 18:35:21 -08:00
Morgan McMillian a4b71c1707 show attachments with thumbnails and remove button 2018-02-19 11:14:50 -08:00
Morgan McMillian e3b4109d7b attach oembed to post 2018-02-19 08:32:51 -08:00
Morgan McMillian f305310bd7 Removed unused http plugin 2018-02-19 07:36:10 -08:00
Morgan McMillian 50b58cc16f file upload to pnut 2018-02-19 07:04:16 -08:00
Morgan McMillian 5026760fa0 Sync up cordova plugins but revert cordova-android due to cli bug 2018-02-19 07:03:47 -08:00
Morgan McMillian fc089fe12b Added plugin cordova-plugin-filechooser
Added image button to new post screen
2018-02-17 15:32:24 -08:00
Morgan McMillian e3938a2efe convert posts which exceed 254 characters to raw "nl.chimpnut.blog.post" resolves #51 2018-02-17 09:28:10 -08:00
Morgan McMillian d0f69b88a9 updated package dependencies 2018-02-17 07:47:07 -08:00
Morgan McMillian f02c4d6220 close the overflow after sharing 2018-01-19 05:58:17 -08:00
Morgan McMillian 5a016ea197 Don't include the / mark unless there are additional mentions resolves #46 2018-01-18 21:36:24 -08:00
Morgan McMillian 576f8ed493 display long posts in timeline, issue #51 2018-01-17 20:41:57 -08:00
Morgan McMillian 399e5e68f8 Enable share post button, issue #13 2017-12-19 19:48:38 -08:00
Morgan McMillian 12e7c24a3d pull blackberry10 target from build 2017-12-19 17:05:10 -08:00
Morgan McMillian 9cfaaae6cb recognize device platform amazon-fireos as Android
resolves #47
2017-11-20 07:03:08 -08:00
Morgan McMillian e6eb71003b don't bother parsing mentions if there are not any.
resolves #46
2017-11-19 14:50:55 -08:00
Morgan McMillian 72c2bcf6c9 bump pnut-butter to latest release 2017-11-19 08:13:30 -08:00
Morgan McMillian 2141764402 remove packages not needed 2017-09-23 15:59:21 -07:00
Morgan McMillian db9cd43d58 Added setting for default timeline, issue #40 2017-09-05 20:50:16 -07:00
Morgan McMillian d0269b11a3 ionic update 2017-09-05 20:49:56 -07:00
Morgan McMillian 154900d497 fix background for white transparent images, issue #41 2017-08-13 07:28:32 -07:00
Morgan McMillian a5ff687946 Add overflow menu to the thread view, issue #39 2017-08-13 07:07:15 -07:00
Morgan McMillian 83d1ff2555 wire up cc on reply setting and refresh timeline on back event 2017-08-05 08:59:22 -07:00
Morgan McMillian 3f57ebe195 fix production compile errors for settings page #31 2017-08-05 07:43:19 -07:00
Morgan McMillian 1d927ce58f version bump 2017-08-04 17:11:26 -07:00
Morgan McMillian 384ad6ad75 Added delete button with overflow menu. Issue #19 2017-08-04 16:43:48 -07:00
Morgan McMillian 40d9f12f79 add parameter to exclude deleted posts when fetching, issue #37 and #35 2017-08-04 13:47:37 -07:00
Morgan McMillian b11ea8e215 added setting for unified timeline, issue #29 and #31 2017-08-04 13:22:13 -07:00
Morgan McMillian e9176adb80 ionic update 2017-08-04 13:21:40 -07:00
Morgan McMillian 73f2a297ab Initial settings page, issue #31 2017-07-23 07:48:29 -07:00
Morgan McMillian 61024e18ca fix bookmark and repost from thread view issue #36 2017-07-23 06:51:37 -07:00
Morgan McMillian 7e58365dac properly exclude deleted posts from the stream #35 2017-07-22 08:17:35 -07:00
Morgan McMillian e1a81b4185 exclude self mention on reply to self #33 2017-07-22 07:15:33 -07:00
Morgan McMillian 471a255b78 fixed hyperlinks on posts in conversation view #34 2017-07-22 07:08:08 -07:00
Morgan McMillian dde9644f48 ionic update 2017-07-22 07:07:46 -07:00
86 changed files with 11822 additions and 658 deletions

6
.gitignore vendored
View file

@ -13,6 +13,7 @@ log.txt
npm-debug.log*
.idea/
.sourcemaps/
.sass-cache/
.tmp/
.versions/
@ -22,10 +23,7 @@ node_modules/
tmp/
temp/
hooks/
platforms/
plugins/
plugins/android.json
plugins/ios.json
www/
$RECYCLE.BIN/
@ -35,3 +33,5 @@ UserInterfaceState.xcuserstate
# other bits
pnut-oauth.ts
src/pages/login/pnutauth.ts
platforms/

140
CHANGELOG.md Normal file
View file

@ -0,0 +1,140 @@
# Changelog
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/).
## [Unreleased]
### 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
- Missing status bar when app is loaded
### Changed
- Updated android platform requirements
- Updated project dependencies
## [0.6.1] - 2018-02-14
### Fixed
- Login using multi-factor authentication
## [0.6.0] - 2018-02-24
### Added
- Convert posts exceeding 254 characters to long post format
- File uploads
### Changed
- Updated project dependencies
- Display login page when required scope changes
### Fixed
- Handle invalid auth tokens
## [0.5.0] - 2018-01-19
### Added
- Option to select default timeline
- Render long posts in timeline
- Share posts outside of Goober
### Fixed
- Authentication on Kindle Fire OS
- Extraneous / being tacked onto individual replies
- Handle white transparent avatars like @c does
- Missing overflow menu in conversation view
## [0.4.0] - 2017-08-05
### Added
- Ability to delete posts
- Settings page
- Option for unified timeline
- Option to cc on a reply-all
### Fixed
- Bookmarks and reposts from conversation view
- Hyperlinks in conversation view
- Deleted posts showing as empty in timeline
- Self mention on replies
## [0.3.0] - 2017-07-09
### Added
- Toast resposnes when completing actions (post, repost, bookmark)
- Highlight posts mentioning you
### Changed
- Updated oauth workflow to work with other device platforms
- Updated order of menu and action buttons
### Fixed
- Show reposts from originator
## [0.2.0] - 2017-06-20
### Added
- Mentions page
- Bookmarks page
- Character count on new post form
### Fixed
- Post format
- Make hyperlinks active
- New post window size
- Cleaned up login page
## [0.1.1] - 2017-06-11
### Fixed
- Focus cursor on text area when composing a post or reply
- Enable spell check and auto complete
## [0.1.0] - 2017-06-11
### Added
- Intial pre-release for Android
[Unreleased]: https://gitlab.dreamfall.space/thrrgilag/Goober/compare/0.7.0...HEAD
[0.7.0]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/0.7.0
[0.6.3]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/0.6.3
[0.6.2]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_6_2
[0.6.1]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_6_1
[0.6.0]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_6_0
[0.5.0]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_5_0
[0.4.0]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_4_0
[0.3.0]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_3_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

35
Makefile Normal file
View file

@ -0,0 +1,35 @@
PKG_ID = com.monkeystew.goober_m
OUT_DIR = platforms/android/app/build/outputs/apk/release
UNALIGNED = $(OUT_DIR)/app-release-unsigned.apk
ALIGNED = $(OUT_DIR)/app-release-unsigned-aligned.apk
APK = $(OUT_DIR)/goober.apk
KEYSTORE = ~/android-keystore.jks
lab:
ionic serve -lc
clean: wwwclean
wwwclean:
rm -r www
distclean:
rm -r node_modules platforms plugins www
init:
npm install
ionic cordova platform add browser
ut:
npm run ionic:build
rm www/manifest.json
cd ubuntutouch && clickable
pwa:
ionic build --prod
cp resources/*.png www/assets/icon
cp -r www ~/opt/Goober

View file

@ -1,9 +1,12 @@
# Goober, a mobile app for pnut.io
Copyright 2017 Morgan McMillian
Copyright 2017 - 2018 Morgan McMillian
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
@ -33,16 +36,12 @@ npm install -g ionic cordova
## Platform build dependencies
#### Android
* Install Java Development Kit
* https://java.com
* Install either Android Studio or the command line tools
* https://developer.android.com/studio/index.html#downloads
#### BlackBerry 10
* Install the either the Native SDK or the WebWorks SDK
* http://developer.blackberry.com/develop/platform_choice/bb10.html
#### Windows
* Install VisualStudio
* https://www.visualstudio.com/
* Install Gradle (needed if you only installed the command line tools)
* https://gradle.org/
## Other build dependencies
@ -54,7 +53,15 @@ npm install
```
## Build and run
Install at least one target platform
```bash
ionic cordova platform add android
```
## Build and run using ionic framework
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
```bash
@ -63,7 +70,7 @@ ionic serve --lab
#### Android
```bash
ionic cordova platform add android # if not yet added
ionic cordova platform add android
ionic cordova build android
ionic cordova run android
```

View file

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="com.monkeystew.goober-m" version="0.3.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.8.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>Goober</name>
<description>Goober, a mobile app for pnut.io</description>
<author email="gilag@monkeystew.com" href="https://monkeystew.org">Morgan McMillian</author>
@ -15,7 +15,7 @@
<preference name="webviewbounce" value="false" />
<preference name="UIWebViewBounce" value="false" />
<preference name="DisallowOverscroll" value="true" />
<preference name="android-minSdkVersion" value="16" />
<preference name="android-minSdkVersion" value="19" />
<preference name="BackupWebStorage" value="none" />
<preference name="SplashMaintainAspectRatio" value="true" />
<preference name="FadeSplashScreenDuration" value="300" />
@ -84,13 +84,19 @@
<icon height="110" src="resources/icon-110.png" width="110" />
<icon height="144" src="resources/icon-144.png" width="144" />
</platform>
<engine name="android" spec="^6.2.3" />
<engine name="blackberry10" spec="^3.8.0" />
<plugin name="cordova-plugin-console" spec="^1.0.5" />
<plugin name="cordova-plugin-device" spec="^1.1.4" />
<plugin name="cordova-plugin-inappbrowser" spec="^1.7.1" />
<plugin name="cordova-plugin-splashscreen" spec="^4.0.3" />
<plugin name="cordova-plugin-statusbar" spec="^2.2.2" />
<plugin name="cordova-plugin-whitelist" spec="^1.3.1" />
<plugin name="ionic-plugin-keyboard" spec="^2.2.1" />
<plugin name="cordova-plugin-filechooser" spec="^1.0.1" />
<plugin name="cordova-plugin-share-content" spec="^1.0.0" />
<plugin name="cordova-plugin-file" spec="^6.0.1" />
<plugin name="cordova-plugin-file-transfer" spec="^1.7.1" />
<plugin name="com-darryncampbell-cordova-plugin-intent" spec="0.0.19" />
<plugin name="cordova-plugin-inappbrowser" spec="^2.0.2" />
<plugin name="cordova-plugin-whitelist" spec="^1.3.3" />
<plugin name="cordova-plugin-splashscreen" spec="^5.0.2" />
<plugin name="cordova-plugin-device" spec="^2.0.1" />
<plugin name="cordova-android-support-gradle-release" spec="^1.4.2">
<variable name="ANDROID_SUPPORT_VERSION" value="27.+" />
</plugin>
<plugin name="cordova-plugin-filepath" spec="^1.3.0" />
<plugin name="cordova-sqlite-storage" spec="2.5.1" />
<allow-navigation href="http://192.168.1.72:8100" />
</widget>

View file

@ -1,5 +1,8 @@
{
"name": "Goober",
"app_id": "com.monkeystew.goober-m",
"type": "ionic-angular"
"type": "ionic-angular",
"integrations": {
"cordova": {}
}
}

9867
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,68 +1,84 @@
{
"name": "Goober",
"version": "0.0.1",
"author": "Ionic Framework",
"homepage": "http://ionicframework.com/",
"private": true,
"scripts": {
"clean": "ionic-app-scripts clean",
"build": "ionic-app-scripts build",
"lint": "ionic-app-scripts lint",
"ionic:build": "ionic-app-scripts build",
"ionic:serve": "ionic-app-scripts serve"
"name": "Goober",
"version": "0.0.1",
"author": "Ionic Framework",
"homepage": "http://ionicframework.com/",
"private": true,
"scripts": {
"clean": "ionic-app-scripts clean",
"build": "ionic-app-scripts build",
"lint": "ionic-app-scripts lint",
"ionic:build": "ionic-app-scripts build",
"ionic:serve": "ionic-app-scripts serve"
},
"dependencies": {
"@angular/common": "5.2.9",
"@angular/compiler": "5.2.9",
"@angular/compiler-cli": "5.2.9",
"@angular/core": "5.2.9",
"@angular/forms": "5.2.9",
"@angular/http": "5.2.9",
"@angular/platform-browser": "5.2.9",
"@angular/platform-browser-dynamic": "5.2.9",
"@ionic-native/core": "4.9.0",
"@ionic-native/device": "4.9.0",
"@ionic-native/file": "4.9.0",
"@ionic-native/file-chooser": "4.9.0",
"@ionic-native/file-path": "4.9.0",
"@ionic-native/file-transfer": "4.9.0",
"@ionic-native/splash-screen": "4.9.0",
"@ionic-native/status-bar": "4.9.0",
"@ionic/storage": "2.1.3",
"com-darryncampbell-cordova-plugin-intent": "^1.1.1",
"cordova-android-support-gradle-release": "^1.4.7",
"cordova-browser": "6.0.0",
"cordova-plugin-device": "^2.0.2",
"cordova-plugin-file": "^6.0.1",
"cordova-plugin-file-transfer": "^1.7.1",
"cordova-plugin-filechooser": "^1.0.1",
"cordova-plugin-filepath": "^1.4.2",
"cordova-plugin-inappbrowser": "^3.0.0",
"cordova-plugin-share-content": "^1.0.0",
"cordova-plugin-splashscreen": "^5.0.2",
"cordova-plugin-telerik-imagepicker": "^2.1.8",
"cordova-plugin-whitelist": "^1.3.3",
"cordova-sqlite-storage": "^2.5.1",
"ionic-angular": "3.9.2",
"ionicons": "4.2.4",
"moment": "^2.18.1",
"ng2-cordova-oauth": "0.0.8",
"ngx-clipboard": "^11.1.9",
"pnut-butter": "^0.21.0",
"run": "^1.4.0",
"rxjs": "5.5.8",
"sw-toolbox": "3.6.0",
"zone.js": "0.8.26"
},
"devDependencies": {
"@ionic/app-scripts": "3.1.10",
"ionic": "3.20.0",
"typescript": "2.8.3"
},
"description": "An Ionic project",
"cordova": {
"plugins": {
"cordova-plugin-share-content": {},
"cordova-plugin-filechooser": {},
"cordova-plugin-file": {},
"cordova-plugin-file-transfer": {},
"com-darryncampbell-cordova-plugin-intent": {},
"cordova-plugin-inappbrowser": {},
"cordova-plugin-whitelist": {},
"cordova-plugin-splashscreen": {},
"cordova-plugin-device": {},
"cordova-android-support-gradle-release": {
"ANDROID_SUPPORT_VERSION": "27.+"
},
"cordova-plugin-filepath": {},
"cordova-sqlite-storage": {}
},
"dependencies": {
"@angular/common": "4.1.2",
"@angular/compiler": "4.1.2",
"@angular/compiler-cli": "4.1.2",
"@angular/core": "4.1.2",
"@angular/forms": "4.1.2",
"@angular/http": "4.1.2",
"@angular/platform-browser": "4.1.2",
"@angular/platform-browser-dynamic": "4.1.2",
"@ionic-native/core": "3.10.2",
"@ionic-native/splash-screen": "3.10.2",
"@ionic-native/status-bar": "3.10.2",
"@ionic/storage": "2.0.1",
"cordova-android": "^6.2.3",
"cordova-blackberry10": "^3.8.0",
"cordova-plugin-console": "^1.0.5",
"cordova-plugin-device": "^1.1.4",
"cordova-plugin-inappbrowser": "^1.7.1",
"cordova-plugin-splashscreen": "^4.0.3",
"cordova-plugin-statusbar": "^2.2.2",
"cordova-plugin-whitelist": "^1.3.1",
"ionic-angular": "3.3.0",
"ionic-plugin-keyboard": "^2.2.1",
"ionicons": "3.0.0",
"moment": "^2.18.1",
"ng2-cordova-oauth": "0.0.8",
"pnut-butter": "^0.8.1",
"run": "^1.4.0",
"rxjs": "5.1.1",
"sw-toolbox": "3.6.0",
"zone.js": "0.8.11"
},
"devDependencies": {
"@ionic/app-scripts": "1.3.7",
"@ionic/cli-plugin-cordova": "1.3.0",
"@ionic/cli-plugin-ionic-angular": "1.3.0",
"typescript": "2.3.3"
},
"description": "An Ionic project",
"cordova": {
"plugins": {
"cordova-plugin-console": {},
"cordova-plugin-device": {},
"cordova-plugin-splashscreen": {},
"cordova-plugin-statusbar": {},
"cordova-plugin-whitelist": {},
"ionic-plugin-keyboard": {},
"cordova-plugin-inappbrowser": {}
},
"platforms": [
"android",
"blackberry10"
]
}
"platforms": [
"browser"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

BIN
resources/icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
resources/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
resources/icon-ipad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
resources/icon-iphone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

1
resources/icon.png.md5 Normal file
View file

@ -0,0 +1 @@
b72de713e098078c68cbffd6ab82439b

1
resources/splash.png.md5 Normal file
View file

@ -0,0 +1 @@
7ce07128c050f41eb46a94eaddd55d4a

View file

@ -1,12 +1,17 @@
import { Component, ViewChild } from '@angular/core';
import { Nav, Platform } from 'ionic-angular';
import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen';
import { Storage } from '@ionic/storage';
import { Device } from '@ionic-native/device';
import { LoginPage } from '../pages/login/login';
import { StreamPage } from '../pages/stream/stream';
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';
@ -17,20 +22,23 @@ export class MyApp {
@ViewChild(Nav) nav: Nav;
rootPage: any = StreamPage;
pages: Array<{title: string, icon: string, component: any, params: Object}>;
profile: IUser;
pages: Array<{title: string, component: any, params: Object}>;
constructor(public platform: Platform, public statusBar: StatusBar, public splashScreen: SplashScreen,
constructor(public platform: Platform, public splashScreen: SplashScreen,
private storage: Storage, private device: Device) {
this.initializeApp();
// used for an example of ngFor and navigation
this.pages = [
{ title: 'Timeline', component: StreamPage, params: {stream: 'personal'} },
{ title: 'Mentions', component: StreamPage, params: {stream: 'mentions'} },
{ title: 'Global', component: StreamPage, params: {stream: 'global'} },
{ title: 'Bookmarks', component: StreamPage, params: {stream: 'bookmarks'} },
{ title: 'Logout', component: {}, params: {}},
{ title: 'Timeline', icon: 'home', component: StreamPage, params: {stream: 'personal'} },
{ title: 'Mentions', icon: 'at', component: StreamPage, params: {stream: 'mentions'} },
{ title: 'Global', icon: 'globe', component: StreamPage, params: {stream: 'global'} },
{ title: 'Bookmarks', icon: 'bookmarks', component: StreamPage, params: {stream: 'bookmarks'} },
{ title: 'Profile', icon: 'person', component: ProfilePage, params: {}},
{ title: 'Settings', icon: 'settings', component: SettingsPage, params: {}},
{ title: 'About', icon: 'information-circle', component: AboutPage, params: {}},
{ title: 'Logout', icon: 'exit', component: {}, params: {}},
];
}
@ -38,35 +46,65 @@ export class MyApp {
initializeApp() {
console.log('--- initializeApp ---');
this.platform.ready().then(() => {
// Okay, so the platform is ready and our plugins are available.
// Here you can do any higher level native things you might need.
this.storage.get('token').then((val) => {
this.storage.get('timeline').then((val) => {
if (val.length > 1) {
pnut.token = val;
this.nav.setRoot(StreamPage, {stream: 'personal'});
this.initialPage(val);
} else {
console.log('ERR WHUT?');
}
}).catch(err => {
console.log('ERROR: ' + err);
this.nav.setRoot(LoginPage);
this.initialPage('personal');
});
this.statusBar.styleDefault();
this.splashScreen.hide();
console.log('---');
console.log(this.device.platform);
console.log('---');
// console.log('---');
// console.log(this.device.platform);
// console.log('---');
});
}
openPage(page) {
initialPage(timeline) {
this.storage.get('token').then((val) => {
if (val.length > 1) {
pnut.token = val;
this.storage.get('scope').then((sval) => {
if (JSON.stringify(sval) !== JSON.stringify(pnutauth.scope)) {
this.nav.setRoot(LoginPage, {});
} else {
this.nav.setRoot(StreamPage, {stream: timeline});
}
// this.nav.setRoot(StreamPage, {stream: timeline});
}).catch(err => {
this.nav.setRoot(LoginPage, {});
});
// this.nav.setRoot(StreamPage, {stream: timeline});
}
}).catch(err => {
console.log('ERROR: ' + err);
this.nav.setRoot(LoginPage, {});
});
}
async openPage(page) {
// Reset the content nav to have just this page
// we wouldn't want the back button to show in this scenario
if (page.title === 'Logout') {
this.storage.remove('token');
this.nav.setRoot(LoginPage);
// this.storage.remove('token');
this.storage.clear();
this.nav.setRoot(LoginPage, {});
} else if (page.title === 'Settings' || page.title === 'About') {
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);
} else {
this.nav.setRoot(page.component, page.params);
}

View file

@ -8,7 +8,8 @@
<ion-content>
<ion-list>
<button menuClose ion-item *ngFor="let p of pages" (click)="openPage(p)">
{{p.title}}
<ion-icon name="{{p.icon}}"></ion-icon>
<span class="menuText">{{p.title}}</span>
</button>
</ion-list>
</ion-content>
@ -16,4 +17,4 @@
</ion-menu>
<!-- 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,18 +1,30 @@
import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { HttpClientModule } from '@angular/common/http';
import { MyApp } from './app.component';
import { LoginPage } from '../pages/login/login';
import { StreamPage, NewPostModal, PostMenu } from '../pages/stream/stream';
import { StreamPage } 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 { 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 { IonicStorageModule } from '@ionic/storage';
import { Device } from '@ionic-native/device';
import { FileChooser } from '@ionic-native/file-chooser';
import { FilePath } from '@ionic-native/file-path';
import { FileTransfer, FileUploadOptions, FileTransferObject } from '@ionic-native/file-transfer';
import { TimeagoPipe } from '../pipes/timeago/timeago';
import { ParserPipe } from '../pipes/parser/parser';
import { ClipboardModule } from 'ngx-clipboard';
@NgModule({
declarations: [
@ -20,15 +32,23 @@ import { ParserPipe } from '../pipes/parser/parser';
LoginPage,
StreamPage,
ThreadPage,
SettingsPage,
AboutPage,
ProfilePage,
UserListPage,
TimeagoPipe,
NewPostModal,
PostMenu,
ParserPipe
ProfileMenu,
ParserPipe,
PostComponent
],
imports: [
BrowserModule,
HttpClientModule,
IonicModule.forRoot(MyApp),
IonicStorageModule.forRoot(),
ClipboardModule,
],
bootstrap: [IonicApp],
entryComponents: [
@ -36,13 +56,21 @@ import { ParserPipe } from '../pipes/parser/parser';
LoginPage,
StreamPage,
ThreadPage,
SettingsPage,
AboutPage,
ProfilePage,
UserListPage,
NewPostModal,
PostMenu
PostMenu,
ProfileMenu
],
providers: [
StatusBar,
SplashScreen,
Device,
FileChooser,
FilePath,
FileTransfer,
FileTransferObject,
{provide: ErrorHandler, useClass: IonicErrorHandler}
]
})

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
src/assets/icon/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,95 @@
<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

@ -0,0 +1,14 @@
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;
}
}

120
src/components/post/post.ts Normal file
View file

@ -0,0 +1,120 @@
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,26 +2,31 @@
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Ionic App</title>
<title>Goober</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="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<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">
<meta name="theme-color" content="#4e8ef7">
<!-- cordova.js required for cordova apps -->
<script src="cordova.js"></script>
<!-- un-comment this code to enable service worker
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js')
.then(() => console.log('service worker installed'))
.catch(err => console.error('Error', err));
}
</script>-->
</script>
<link href="build/main.css" rel="stylesheet">
@ -34,6 +39,8 @@
<!-- The polyfills js is generated during the build process -->
<script src="build/polyfills.js"></script>
<script src="build/vendor.js"></script>
<!-- The bundle js is generated during the build process -->
<script src="build/main.js"></script>

View file

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

33
src/models/IUser.ts Normal file
View file

@ -0,0 +1,33 @@
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

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

View file

@ -0,0 +1,11 @@
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

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

View file

@ -0,0 +1,43 @@
<!--
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

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

30
src/pages/about/about.ts Normal file
View file

@ -0,0 +1,30 @@
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,17 +20,12 @@
<h2>Goober</h2>
<p block>A mobile client for pnut.io</p>
</div><p>&nbsp;</p>
<div *ngIf="!showToken">
<p>Tap the Log In button to open browser window and enter your pnut.io creditionals and authorize Goober.</p>
<div *ngIf="oob">
<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>
</div>
<button ion-button block (click)="login()">Log In</button><p>
</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-label stacked>Username</ion-label>
<ion-input [(ngModel)]="username" type="text"></ion-input>
<ion-label stacked>Password</ion-label>
<ion-input [(ngModel)]="password" type="password"></ion-input>
<button ion-button block (click)="login()">Log In</button>
</ion-content>

View file

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

View file

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

View file

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

View file

@ -0,0 +1,76 @@
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

@ -0,0 +1,86 @@
<!--
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

@ -0,0 +1,14 @@
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

@ -0,0 +1,159 @@
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

@ -0,0 +1,52 @@
<!--
Generated template for the SettingsPage page.
See http://ionicframework.com/docs/components/#navigation for more info on
Ionic pages and navigation.
-->
<ion-header>
<ion-navbar>
<ion-title>settings</ion-title>
</ion-navbar>
</ion-header>
<ion-content overflow-scroll=”true”>
<ion-list>
<ion-item>
<ion-label>Default Timeline</ion-label>
<ion-select [(ngModel)]="set_default" (ionChange)="updateDefault()" interface="popover">
<ion-option value="personal">Home Timeline</ion-option>
<ion-option value="global">Global Timeline</ion-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label>
<h2>Unified Home</h2>
<p>Include mentions in home timeline</p>
</ion-label>
<ion-toggle [(ngModel)]="set_unified" (ionChange)="updateUnified()"></ion-toggle>
</ion-item>
<ion-item>
<ion-label>
<h2>Use CC on Reply All</h2>
<p>Additional mentions follow /</p>
</ion-label>
<ion-toggle [(ngModel)]="set_cc" (ionChange)="updateCc()"></ion-toggle>
</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-content>

View file

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

View file

@ -0,0 +1,73 @@
import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { Storage } from '@ionic/storage';
import { Events } from 'ionic-angular';
/**
* Generated class for the SettingsPage page.
*
* See http://ionicframework.com/docs/components/#navigation for more info
* on Ionic pages and navigation.
*/
@Component({
selector: 'page-settings',
templateUrl: 'settings.html',
})
export class SettingsPage {
private set_unified: boolean;
private set_cc: boolean;
private set_default: string;
private set_hideimg: boolean;
constructor(public navCtrl: NavController, private storage: Storage, public navParams: NavParams,
public events: Events) {
}
ngAfterViewInit() {
this.storage.get('timeline').then((val) => {
this.set_default = val;
}).catch(err => {
console.log('ERROR: ' +err);
});
this.storage.get('unified').then((val) => {
this.set_unified = val;
}).catch(err => {
console.log('ERROR: ' + err);
});
this.storage.get('cc').then((val) => {
this.set_cc = val;
}).catch(err => {
console.log('ERROR: ' + err);
});
this.storage.get('hideimg').then((val) => {
this.set_hideimg = val;
}).catch(err => {
console.log('ERROR: ' + err);
});
}
ionViewDidLeave() {
this.events.publish('stream:reload', {});
}
updateUnified() {
this.storage.set('unified', this.set_unified);
}
updateCc() {
this.storage.set('cc', this.set_cc);
}
updateDefault() {
this.storage.set('timeline', this.set_default);
}
updateHideImg() {
this.storage.set('hideimg', this.set_hideimg);
}
}

View file

@ -1,20 +1,48 @@
<ion-header>
<ion-toolbar>
<ion-title>New Post</ion-title>
<ion-buttons start>
<button ion-button (click)="dismiss()">
<span ion-text color="primary" showWhen="ios">Cancel</span>
<ion-icon name="md-close" showWhen="android,windows"></ion-icon>
<span ion-text color="primary">Cancel</span>
</button>
</ion-buttons>
<ion-buttons end>
<button ion-button>
<span ion-text color="primary" showWhen="ios">Post</span>
<ion-icon name="send" showWhen="android,windows"></ion-icon>
</ion-buttons>
<ion-title>{{ title }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<p>Blarp</p>
<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>

View file

@ -0,0 +1,195 @@
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

@ -0,0 +1,106 @@
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,70 +24,7 @@
<ion-list>
<ion-card *ngFor="let post of posts" color="{{ post.you_are_mentioned ? 'mention' : '' }}">
<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>
</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)">
<ion-icon name="more"></ion-icon>
</button>
</ion-col> -->
</ion-row>
<post [post]="post" [hideImg]="hideImg" [ccOnReply]="ccOnReply" [myUsername]="myUsername"></post>
</ion-card>
</ion-list>

View file

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

View file

@ -1,6 +1,9 @@
import { Component, ViewChild, ChangeDetectorRef } from '@angular/core';
import { ViewController, NavController, NavParams, ModalController, Content, ToastController, PopoverController } from 'ionic-angular';
import { ThreadPage } from '../thread/thread';
import { NavController, NavParams, ModalController, Content } from 'ionic-angular';
import { Storage } from '@ionic/storage';
import { Events } from 'ionic-angular';
import { LoginPage } from '../login/login';
import { NewPostModal } from '../stream/new-post';
import * as pnut from 'pnut-butter';
@ -26,38 +29,66 @@ export class StreamPage {
fcaller: any;
myUsername: string;
showScrollBtn: boolean = false;
showUnified: boolean;
ccOnReply: boolean = false;
hideImg: boolean = false;
constructor(public navCtrl: NavController, public navParams: NavParams,
public modalCtrl: ModalController, private storage: Storage,
private changeDetectorRef: ChangeDetectorRef, public events: Events) {
this.storage.get('cc').then((val) => {
this.ccOnReply = val;
});
this.storage.get('hideimg').then((val) => {
this.hideImg = val;
});
this.storage.get('unified').then((val) => {
this.showUnified = val;
switch (navParams.data.stream) {
case 'global':
this.title = 'Global';
this.fetcher = pnut.global;
this.fetchPosts();
break;
case 'personal':
this.title = 'Timeline';
console.log(this.showUnified);
this.fetcher = this.showUnified ? pnut.unified : pnut.personal;
this.fetchPosts();
break;
case 'mentions':
this.title = 'Mentions';
this.fetcher = pnut.mentions;
this.fetchMyPosts();
break;
case 'bookmarks':
this.title = 'Bookmarks';
this.fetcher = pnut.bookmarks;
this.fetchMyPosts();
break;
}
}).catch(err => {
console.log('ERROR: ' + err);
});
constructor(public navCtrl: NavController, public navParams: NavParams, public modalCtrl: ModalController,
private changeDetectorRef: ChangeDetectorRef, public toastCtrl: ToastController, public popoverCtrl: PopoverController) {
// console.log(JSON.stringify(navParams));
switch (navParams.data.stream) {
case 'global':
this.title = 'Global';
this.fetcher = pnut.global;
this.fetchPosts();
break;
case 'personal':
this.title = 'Timeline';
this.fetcher = pnut.personal;
this.fetchPosts();
break;
case 'mentions':
this.title = 'Mentions';
this.fetcher = pnut.mentions;
this.fetchMyPosts();
break;
case 'bookmarks':
this.title = 'Bookmarks';
this.fetcher = pnut.bookmarks;
this.fetchMyPosts();
break;
}
pnut.user('me').then(res => {
this.myUsername = res.data.username;
}).catch(err => {
console.log(err);
// console.log('-*-');
// console.log(JSON.stringify(err));
});
this.events.subscribe('stream:reload', (event) => {
console.log('-reload-');
this.refreshPage();
});
}
ngAfterViewInit() {
@ -72,8 +103,19 @@ export class StreamPage {
});
}
refreshPage() {
// this.navCtrl.setRoot(this.navCtrl.getActive().component);
this.navCtrl.setRoot(StreamPage, {stream: 'personal'});
}
fetchOlderPosts(infiniteScroll) {
let params = {include_raw: 1, include_reposted_by:1, before_id: this.before_id, count: 40};
let params = {
include_deleted: 0,
include_raw: 1,
include_reposted_by:1,
before_id: this.before_id,
count: 40
};
if (this.title === 'Mentions') {
this.fcaller = this.fetcher('me', params);
} else {
@ -81,18 +123,7 @@ export class StreamPage {
}
this.fcaller.then(res => {
if (res.data.length > 0) {
for (var i = 0; i < res.data.length; i++) {
if (res.data[i]['repost_of']) {
res.data[i] = res.data[i]['repost_of']
}
for (var j = 0; j < res.data[i]['content']['entities']['mentions'].length; j++) {
var men = res.data[i]['content']['entities']['mentions'][j]['text'];
if (this.myUsername === men) {
res.data[i]['you_are_mentioned'] = true;
}
}
}
this.posts.push.apply(this.posts, res.data);
this.posts.push.apply(this.posts, this.parseData(res.data));
this.before_id = res.meta.min_id;
}
console.log('since_id: ' + this.since_id);
@ -104,7 +135,13 @@ export class StreamPage {
}
fetchNewerPosts(refresher) {
let params = {include_raw: 1, include_reposted_by: 1, since_id: this.since_id, count: 40};
let params = {
include_deleted: 0,
include_raw: 1,
include_reposted_by: 1,
since_id: this.since_id,
count: 40
};
if (this.title === 'Mentions') {
this.fcaller = this.fetcher('me', params);
} else {
@ -112,18 +149,7 @@ export class StreamPage {
}
this.fcaller.then(res => {
if (res.data.length > 0) {
for (var i = 0; i < res.data.length; i++) {
if (res.data[i]['repost_of']) {
res.data[i] = res.data[i]['repost_of']
}
for (var j = 0; j < res.data[i]['content']['entities']['mentions'].length; j++) {
var men = res.data[i]['content']['entities']['mentions'][j]['text'];
if (this.myUsername === men) {
res.data[i]['you_are_mentioned'] = true;
}
}
}
Array.prototype.unshift.apply(this.posts, res.data);
Array.prototype.unshift.apply(this.posts, this.parseData(res.data));
this.since_id = res.meta.max_id;
}
console.log('since_id: ' + this.since_id);
@ -135,271 +161,76 @@ export class StreamPage {
}
fetchPosts() {
this.fetcher({include_raw: 1, include_reposted_by: 1, count: 40}).then(res => {
for (var i = 0; i < res.data.length; i++) {
if (res.data[i]['repost_of']) {
res.data[i] = res.data[i]['repost_of']
var reposted_by_string = "";
for (var j = 0; j < res.data[i]['reposted_by'].length; j++) {
reposted_by_string = reposted_by_string + res.data[i]['reposted_by'][j]['username'] + ", ";
}
// res.data[i]['reposted_by_string'] = "Reposted by: " + reposted_by_string;
}
if (res.data[i].content) {
for (var j = 0; j < res.data[i]['content']['entities']['mentions'].length; j++) {
var men = res.data[i]['content']['entities']['mentions'][j]['text'];
if (this.myUsername === men) {
res.data[i]['you_are_mentioned'] = true;
}
}
}
let params = {
include_deleted: 0,
include_raw: 1,
include_reposted_by: 1,
count: 40
};
this.fetcher(params).then(res => {
if (res.meta.code === 401) {
this.storage.clear();
this.navCtrl.setRoot(LoginPage);
} else {
this.posts = this.parseData(res.data);
this.since_id = res.meta.max_id;
this.before_id = res.meta.min_id;
console.log('since_id: ' + this.since_id);
console.log('before_id: ' + this.before_id);
}
this.posts = res.data;
this.since_id = res.meta.max_id;
this.before_id = res.meta.min_id;
console.log('since_id: ' + this.since_id);
console.log('before_id: ' + this.before_id);
}).catch(err => {
console.log(err);
});
}
fetchThread(threadid) {
pnut.thread(threadid, {include_raw: 1, count: 140}).then(res => {
this.navCtrl.push(ThreadPage, {posts: res.data});
}).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 = "";
for (var j = 0; j < data[i]['reposted_by'].length; j++) {
reposted_by_string = reposted_by_string + data[i]['reposted_by'][j]['username'] + ", ";
}
// data[i]['reposted_by_string'] = "Reposted by: " + reposted_by_string;
}
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;
}
fetchMyPosts() {
console.log('-- fetching mentions --');
this.fetcher('me', {include_raw: 1, count: 40}).then(res => {
this.posts = res.data;
this.since_id = res.meta.max_id;
this.before_id = res.meta.min_id;
console.log('since_id: ' + this.since_id);
console.log('before_id: ' + this.before_id);
}).catch(err => {
console.log(err);
});
}
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;
}
if (res.meta.code === 401) {
this.storage.clear();
this.navCtrl.setRoot(LoginPage);
} else {
this.posts = res.data;
this.since_id = res.meta.max_id;
this.before_id = res.meta.min_id;
console.log('since_id: ' + this.since_id);
console.log('before_id: ' + this.before_id);
}
}).catch(err => {
console.log(err);
});
}
showNewPost() {
let newPostModal = this.modalCtrl.create(NewPostModal);
let newPostModal = this.modalCtrl.create(NewPostModal, {me: this.myUsername});
newPostModal.present();
}
showReplyPost(postData) {
let newPostModal = this.modalCtrl.create(NewPostModal, {type: 'reply', post: postData, me: this.myUsername});
newPostModal.present();
}
showQuotedPost(postData) {
console.log(postData);
let newPostModal = this.modalCtrl.create(NewPostModal, {type: 'quote', post: postData, me: this.myUsername});
newPostModal.present();
}
presentToast(text) {
let toast = this.toastCtrl.create({
position: 'top',
message: text,
duration: 2000
});
toast.present();
}
presentPostMenu(myEvent) {
let popover = this.popoverCtrl.create(PostMenu);
popover.present({
ev: myEvent
});
}
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 col-2><div text-center>{{254 - ptext.length}}</div></ion-col>
<ion-col col-2>
<button ion-button (click)="send()">
<ion-icon name="send"></ion-icon>
</button>
</ion-col>
</ion-row>
</ion-card>
</ion-content>
`
})
export class NewPostModal {
title: string;
replyid: string;
ptext: string = "";
options: Object;
myUsername: string;
constructor(public navParams: NavParams, public viewCtrl: ViewController, public toastCtrl: ToastController) {
console.log(this.navParams);
this.myUsername = navParams.data.me;
if (navParams.data.type === 'reply') {
this.replyid = navParams.data.post.id;
this.options = {replyTo: this.replyid};
this.ptext = "@" + navParams.data.post.user.username + " ";
if (navParams.data.post.content.entities) {
if (navParams.data.post.content.entities.mentions) {
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";
}
}
dismiss() {
this.viewCtrl.dismiss();
}
send() {
console.log(this.ptext);
pnut.createPost(this.ptext, this.options).then(res => {
console.log(res);
this.presentToast("Status posted.");
}).catch(err => {
console.log(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) {
return mtext;
} else {
return "";
}
}
}
@Component({
template: `
<ion-list>
<button ion-item disabled (click)="close()">Share</button>
<button ion-item *ngIf="showDelBtn" (click)="close()">Delete</button>
</ion-list>
`
})
export class PostMenu {
showDelBtn: boolean = false;
constructor(public navParams: NavParams, public viewCtrl: ViewController) {}
// buttonState() {
// return this.showDelBtn ? _.state : _;
// }
close() {
this.viewCtrl.dismiss();
}
}

View file

@ -15,69 +15,11 @@
</ion-header>
<ion-content>
<ion-list>
<ion-card *ngFor="let post of posts">
<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>
<p>{{ post.is_deleted ? '{post deleted}' : post.content.text }}</p>
</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>
<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>
<ion-icon name="more"></ion-icon>
</button>
</ion-col> -->
</ion-row>
<post [post]="post" [hideImg]="hideImg" [ccOnReply]="ccOnReply" [myUsername]="myUsername"></post>
</ion-card>
</ion-list>

View file

@ -1,3 +1,14 @@
page-thread {
.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,8 +1,8 @@
import { Component } from '@angular/core';
import { NavController, NavParams, ModalController } from 'ionic-angular';
import { NewPostModal } from '../stream/stream';
import { NavController, NavParams } from 'ionic-angular';
import { Storage } from '@ionic/storage';
// import * as pnut from 'pnut-butter';
import * as pnut from 'pnut-butter';
/**
* Generated class for the ThreadPage page.
@ -18,20 +18,23 @@ export class ThreadPage {
title: string;
posts: Array<Object> = [];
myUsername: string;
hideImg: boolean = false;
ccOnReply: boolean = false;
constructor(public navCtrl: NavController, public navParams: NavParams, public modalCtrl: ModalController) {
constructor(public navCtrl: NavController, public navParams: NavParams,
private storage: Storage) {
this.posts = this.navParams.data.posts;
}
this.myUsername = this.navParams.data.me;
showReplyPost(postData) {
let newPostModal = this.modalCtrl.create(NewPostModal, {type: 'reply', post: postData});
newPostModal.present();
}
this.storage.get('hideimg').then((val) => {
this.hideImg = val;
});
this.storage.get('cc').then((val) => {
this.ccOnReply = val;
});
showQuotedPost(postData) {
console.log(postData);
let newPostModal = this.modalCtrl.create(NewPostModal, {type: 'quote', post: postData});
newPostModal.present();
}
}

View file

@ -0,0 +1,35 @@
<!--
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

@ -0,0 +1,14 @@
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

@ -0,0 +1,71 @@
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 {
let hregex = /href="([\S]+)"/g;
// value = value.replace(hregex, "class=\"ex-link\" href=");
if (typeof value == "undefined") value = "";
value = value.replace(hregex, "onClick=\"window.open('$1', '_system', 'location=yes')\"");
return this._sanitizer.bypassSecurityTrustHtml(value);
}

View file

@ -1,11 +0,0 @@
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
};
}

1
ubuntutouch/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
build

1
ubuntutouch/LICENSE Symbolic link
View file

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

1
ubuntutouch/README.md Symbolic link
View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
[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

1
ubuntutouch/icon.png Symbolic link
View file

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

15
ubuntutouch/manifest.json Normal file
View file

@ -0,0 +1,15 @@
{
"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"
}

1
ubuntutouch/www Symbolic link
View file

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