Merge pull request #85 from NicolasConstant/develop

0.6.0 Release
This commit is contained in:
Nicolas Constant 2019-04-12 22:33:01 -04:00 committed by GitHub
commit bf438b1b92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1764 additions and 248 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
/release
# compiled output
/release
/dist
/dist-server
/tmp

47
.travis.yml Normal file
View File

@ -0,0 +1,47 @@
sudo: required
dist: trusty
language: c
matrix:
include:
- os: osx
- os: linux
env: CC=clang CXX=clang++ npm_config_clang=1
compiler: clang
node_js:
- 10.9.0
cache:
directories:
- node_modules
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
- icnsutils
- graphicsmagick
- libgnome-keyring-dev
- xz-utils
- xorriso
- xvfb
install:
- nvm install 10.9.0
- npm install electron-builder@next
- npm install
- npm rebuild node-sass
- export DISPLAY=':99.0'
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start &
- sleep 3
script:
- npm run dist

45
appveyor.yml Normal file
View File

@ -0,0 +1,45 @@
os: unstable
cache:
- node_modules
environment:
GH_TOKEN:
secure: wRRBU0GXTmTBgZBs2PGSaEJWOflynAyvp3Nc/7e9xmciPfkUCQAXcpOn0jIYmzpb
matrix:
- nodejs_version: 10.9.0
install:
- ps: Install-Product node $env:nodejs_version
- set CI=true
- npm install -g npm@latest
- set PATH=%APPDATA%\npm;%PATH%
- npm install
matrix:
fast_finish: true
build: off
version: '{build}'
shallow_clone: true
clone_depth: 1
test_script:
- ps: >-
npm run test-nowatch
$wc = New-Object 'System.Net.WebClient'
Get-ChildItem . -Name -Recurse 'TESTS-*.xml' |
Foreach-Object {
$wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $_))
}
- npm run dist
artifacts:
- path: dist
deploy:
- provider: FTP
host: home205977321.1and1-data.host
protocol: sftp
username: u45308485-sengi
password:
secure: Sk3NZwuaYK9hTIQ3kgIIQEc8SmaPDVGvGpgsZzFEzoVLuy4WxVfvKQtegW9oXaj7
folder: /
application: dist.zip
on:
branch: master

BIN
assets/icons/mac/icon.icns Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/icons/png/16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/icons/png/24x24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
assets/icons/png/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/icons/png/48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
assets/icons/png/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/icons/png/96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/icons/win/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -10,7 +10,7 @@ let win
function createWindow() {
// Create the browser window.
win = new BrowserWindow({ width: 393, height: 800, title: "Sengi", backgroundColor: '#FFF' });
win = new BrowserWindow({ width: 377, height: 800, title: "Sengi", backgroundColor: '#FFF', 'useContentSize': true });
var server = http.createServer(requestHandler).listen(9527);
win.loadURL('http://localhost:9527');

1114
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,65 +1,128 @@
{
"name": "sengi",
"version": "0.0.0",
"license": "AGPL-3.0-or-later",
"main": "main-electron.js",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build --prod",
"test": "ng test",
"test-nowatch": "ng test --watch=false",
"lint": "ng lint",
"e2e": "ng e2e",
"electron": "ng build --prod && electron .",
"electron-debug": "ng build && electron ."
},
"private": true,
"dependencies": {
"@angular/animations": "^7.2.7",
"@angular/common": "^7.2.7",
"@angular/compiler": "^7.2.7",
"@angular/core": "^7.2.7",
"@angular/forms": "^7.2.7",
"@angular/http": "^7.2.7",
"@angular/platform-browser": "^7.2.7",
"@angular/platform-browser-dynamic": "^7.2.7",
"@angular/router": "^7.2.7",
"@fortawesome/angular-fontawesome": "^0.3.0",
"@fortawesome/fontawesome-svg-core": "^1.2.13",
"@fortawesome/free-brands-svg-icons": "^5.7.0",
"@fortawesome/free-regular-svg-icons": "^5.7.0",
"@fortawesome/free-solid-svg-icons": "^5.7.0",
"@ngxs/storage-plugin": "^3.2.0",
"@ngxs/store": "^3.2.0",
"bootstrap": "^4.1.3",
"core-js": "^2.5.4",
"electron": "^4.0.6",
"ionicons": "^4.4.3",
"rxjs": "^6.4.0",
"tslib": "^1.9.0",
"zone.js": "^0.8.29"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.13.0",
"@angular/cli": "~7.3.3",
"@angular/compiler-cli": "^7.2.7",
"@angular/language-service": "^7.2.7",
"@types/jasmine": "~2.8.6",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "~4.2.1",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~1.7.1",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.0",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"karma-junit-reporter": "^1.2.0",
"protractor": "~5.3.0",
"ts-node": "~5.0.1",
"tslint": "~5.9.1",
"typescript": "~3.2.4"
}
"name": "sengi",
"version": "0.6.0",
"license": "AGPL-3.0-or-later",
"main": "main-electron.js",
"description": "A multi-account desktop client for Mastodon and Pleroma",
"author": {
"name": "Nicolas Constant",
"email": "github@nicolas-constant.com"
},
"repository": {
"type": "git",
"url": "https://github.com/NicolasConstant/sengi.git"
},
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build --prod",
"test": "ng test",
"test-nowatch": "ng test --watch=false",
"lint": "ng lint",
"e2e": "ng e2e",
"electron": "ng build --prod && electron .",
"electron-debug": "ng build && electron .",
"dist": "npm run build && build --publish onTagOrDraft"
},
"private": true,
"dependencies": {
"@angular/animations": "^7.2.7",
"@angular/common": "^7.2.7",
"@angular/compiler": "^7.2.7",
"@angular/core": "^7.2.7",
"@angular/forms": "^7.2.7",
"@angular/http": "^7.2.7",
"@angular/platform-browser": "^7.2.7",
"@angular/platform-browser-dynamic": "^7.2.7",
"@angular/router": "^7.2.7",
"@fortawesome/angular-fontawesome": "^0.3.0",
"@fortawesome/fontawesome-svg-core": "^1.2.13",
"@fortawesome/free-brands-svg-icons": "^5.7.0",
"@fortawesome/free-regular-svg-icons": "^5.7.0",
"@fortawesome/free-solid-svg-icons": "^5.7.0",
"@ngxs/storage-plugin": "^3.2.0",
"@ngxs/store": "^3.2.0",
"bootstrap": "^4.1.3",
"core-js": "^2.5.4",
"ionicons": "^4.4.3",
"rxjs": "^6.4.0",
"tslib": "^1.9.0",
"zone.js": "^0.8.29"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.13.0",
"@angular/cli": "~7.3.3",
"@angular/compiler-cli": "^7.2.7",
"@angular/language-service": "^7.2.7",
"@types/jasmine": "~2.8.6",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "~4.2.1",
"electron": "^4.0.6",
"electron-builder": "^20.39.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~1.7.1",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.0",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"karma-junit-reporter": "^1.2.0",
"protractor": "~5.3.0",
"ts-node": "~5.0.1",
"tslint": "~5.9.1",
"typescript": "~3.2.4"
},
"optionalDependencies": {
"jquery": "1.9.1 - 3",
"popper.js": "^1.14.7"
},
"build": {
"productName": "Sengi",
"appId": "org.sengi.desktop",
"artifactName": "${productName}-${version}-${os}.${ext}",
"directories": {
"output": "release"
},
"files": [
"dist/",
"node_modules/",
"main-electron.js",
"package.json"
],
"dmg": {
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
]
},
"mac": {
"icon": "assets/icons/mac/icon.icns",
"target": [
"dmg"
],
"category": "public.app-category.social-networking"
},
"win": {
"icon": "assets/icons/win/icon.ico",
"target": "nsis"
},
"linux": {
"icon": "assets/icons/png",
"target": [
"AppImage",
"deb",
"snap"
],
"category": "Network"
}
}
}

View File

@ -63,10 +63,9 @@ export class AppComponent implements OnInit, OnDestroy {
this.dragoverSub = this.dragoverSubject
.pipe(
debounceTime(150)
debounceTime(1500)
)
.subscribe(() => {
console.warn('disable drag');
.subscribe(() => {
this.drag = false;
})
}

View File

@ -34,8 +34,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private maxCharLength: number;
charCountLeft: number;
postCounts: number = 1;
isSending: boolean;
mentionTooFarAwayError: boolean;
@Input() statusReplyingToWrapper: StatusWrapper;
@Output() onClose = new EventEmitter();
@ -76,6 +76,21 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.status += `@${mention} `;
}
switch (this.statusReplyingTo.visibility) {
case 'unlisted':
this.setVisibility(VisibilityEnum.Unlisted);
break;
case 'public':
this.setVisibility(VisibilityEnum.Public);
break;
case 'private':
this.setVisibility(VisibilityEnum.Private);
break;
case 'direct':
this.setVisibility(VisibilityEnum.Direct);
break;
}
this.title = this.statusReplyingTo.spoiler_text;
}
@ -100,60 +115,54 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.notificationService.notifyHttpError(err);
});
this.instancesInfoService.getDefaultPrivacy(selectedAccount)
.then((defaultPrivacy: VisibilityEnum) => {
switch (defaultPrivacy) {
case VisibilityEnum.Public:
this.selectedPrivacy = 'Public';
break;
case VisibilityEnum.Unlisted:
this.selectedPrivacy = 'Unlisted';
break;
case VisibilityEnum.Private:
this.selectedPrivacy = 'Follows-only';
break;
case VisibilityEnum.Direct:
this.selectedPrivacy = 'DM';
break;
}
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
if (!this.statusReplyingToWrapper) {
this.instancesInfoService.getDefaultPrivacy(selectedAccount)
.then((defaultPrivacy: VisibilityEnum) => {
this.setVisibility(defaultPrivacy);
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
}
}
}
mentionTooFarAwayError: boolean;
private setVisibility(defaultPrivacy: VisibilityEnum) {
switch (defaultPrivacy) {
case VisibilityEnum.Public:
this.selectedPrivacy = 'Public';
break;
case VisibilityEnum.Unlisted:
this.selectedPrivacy = 'Unlisted';
break;
case VisibilityEnum.Private:
this.selectedPrivacy = 'Follows-only';
break;
case VisibilityEnum.Direct:
this.selectedPrivacy = 'DM';
break;
}
}
private countStatusChar(status: string) {
this.mentionTooFarAwayError = false;
const parseStatus = this.parseStatus(status);
const mentions = this.getMentionsFromStatus(status);
if(mentions.length > 0){
if (mentions.length > 0) {
let containAllMention = true;
mentions.forEach(m => {
if(!parseStatus[0].includes(m)){
if (!parseStatus[0].includes(m)) {
containAllMention = false;
}
});
if(!containAllMention){
if (!containAllMention) {
this.mentionTooFarAwayError = true;
this.charCountLeft = this.maxCharLength - status.length;
this.postCounts = 1;
return;
}
// const lastMention = mentions[mentions.length - 1];
// const lastMentionPosition = status.lastIndexOf(lastMention);
// console.warn(`lastMentionPosition ${lastMentionPosition}`);
// if(lastMentionPosition > (this.maxCharLength - lastMention.length * 2 + 10)){
// this.mentionTooFarAwayError = true;
// this.charCountLeft = this.maxCharLength - status.length;
// this.postCounts = 1;
// return;
// }
}
const currentStatus = parseStatus[parseStatus.length - 1];
@ -182,6 +191,9 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
globalUniqueMentions.push(mention);
}
const selectedUser = this.toolsService.getSelectedAccounts()[0];
globalUniqueMentions = globalUniqueMentions.filter(x => x.toLowerCase() !== `${selectedUser.username}@${selectedUser.instance}`.toLowerCase());
return globalUniqueMentions;
}
@ -196,7 +208,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.isSending = true;
let visibility: VisibilityEnum = VisibilityEnum.Unknown;
switch (this.selectedPrivacy) { //FIXME: in case of responding, set the visibility to original
switch (this.selectedPrivacy) {
case 'Public':
visibility = VisibilityEnum.Public;
break;
@ -275,7 +287,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
let aggregateMention = '';
let mentions = this.getMentionsFromStatus(status);
mentions.forEach(x => {
aggregateMention += `${x} `;
aggregateMention += `${x} `;
});
const currentMaxCharLength = this.maxCharLength + mentionExtraChars;
@ -290,7 +302,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
return results;
}
private getMentionExtraChars(status: string): number{
private getMentionExtraChars(status: string): number {
let mentionExtraChars = 0;
let mentions = this.getMentionsFromStatus(status);
@ -305,7 +317,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
return mentionExtraChars;
}
private getMentionsFromStatus(status: string): string[]{
private getMentionsFromStatus(status: string): string[] {
return status.split(' ').filter(x => x.indexOf('@') === 0 && x.length > 1);
}
}

View File

@ -1,16 +1,18 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { faTimes } from "@fortawesome/free-solid-svg-icons";
import { NavigationService, LeftPanelType } from '../../services/navigation.service';
import { AccountWrapper } from '../../models/account.models';
import { OpenThreadEvent } from '../../services/tools.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-floating-column',
templateUrl: './floating-column.component.html',
styleUrls: ['./floating-column.component.scss']
})
export class FloatingColumnComponent implements OnInit {
export class FloatingColumnComponent implements OnInit, OnDestroy {
faTimes = faTimes;
overlayActive: boolean;
overlayAccountToBrowse: string;
@ -19,31 +21,59 @@ export class FloatingColumnComponent implements OnInit {
userAccountUsed: AccountWrapper;
openPanel: string;
openPanel: string = '';
private activatedPanelSub: Subscription;
constructor(private readonly navigationService: NavigationService) { }
ngOnInit() {
this.navigationService.activatedPanelSubject.subscribe((type: LeftPanelType) => {
this.activatedPanelSub = this.navigationService.activatedPanelSubject.subscribe((type: LeftPanelType) => {
this.overlayActive = false;
switch (type) {
case LeftPanelType.Closed:
this.openPanel = '';
break;
case LeftPanelType.AddNewAccount:
this.openPanel = 'addNewAccount';
if (this.openPanel === 'addNewAccount') {
this.closePanel();
} else {
this.openPanel = 'addNewAccount';
}
break;
case LeftPanelType.CreateNewStatus:
this.openPanel = 'createNewStatus';
if (this.openPanel === 'createNewStatus') {
this.closePanel();
} else {
this.openPanel = 'createNewStatus';
}
break;
case LeftPanelType.ManageAccount:
this.userAccountUsed = this.navigationService.getAccountToManage();
this.openPanel = 'manageAccount';
let lastUserAccountId = '';
if (this.userAccountUsed && this.userAccountUsed.info) {
lastUserAccountId = this.userAccountUsed.info.id;
}
this.userAccountUsed = this.navigationService.getAccountToManage();
if (this.openPanel === 'manageAccount' && this.userAccountUsed.info.id === lastUserAccountId) {
this.closePanel();
} else {
this.openPanel = 'manageAccount';
}
break;
case LeftPanelType.Search:
this.openPanel = 'search';
if (this.openPanel === 'search') {
this.closePanel();
} else {
this.openPanel = 'search';
}
break;
case LeftPanelType.Settings:
this.openPanel = 'settings';
if (this.openPanel === 'settings') {
this.closePanel();
} else {
this.openPanel = 'settings';
}
break;
default:
this.openPanel = '';
@ -51,6 +81,12 @@ export class FloatingColumnComponent implements OnInit {
});
}
ngOnDestroy(): void {
if(this.activatedPanelSub) {
this.activatedPanelSub.unsubscribe();
}
}
closePanel(): boolean {
this.navigationService.closePanel();
return false;
@ -60,21 +96,21 @@ export class FloatingColumnComponent implements OnInit {
this.overlayAccountToBrowse = account;
this.overlayHashtagToBrowse = null;
this.overlayThreadToBrowse = null;
this.overlayActive = true;
this.overlayActive = true;
}
browseHashtag(hashtag: string): void {
this.overlayAccountToBrowse = null;
this.overlayHashtagToBrowse = hashtag;
this.overlayThreadToBrowse = null;
this.overlayActive = true;
this.overlayActive = true;
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.overlayAccountToBrowse = null;
this.overlayHashtagToBrowse = null;
this.overlayThreadToBrowse = openThreadEvent;
this.overlayActive = true;
this.overlayActive = true;
}
closeOverlay(): boolean {

View File

@ -1,7 +1,8 @@
<div class="my-account__body flexcroll">
<h4 class="my-account__label">add column:</h4>
<a class="my-account__link my-account__blue" href *ngFor="let stream of availableStreams" (click)="addStream(stream)">
{{ stream.name }}
<a class="my-account__link my-account__blue" href *ngFor="let stream of availableStreams"
(click)="addStream(stream)" title="{{ stream.isAdded ? '' : 'add timeline'}}" [class.my-account__link--disabled]="stream.isAdded">
{{ stream.name }} <fa-icon class="my-account__link--icon" *ngIf="stream.isAdded" [icon]="faCheckSquare"></fa-icon>
</a>
<h4 class="my-account__label my-account__margin-top">remove account from sengi:</h4>
<a class="my-account__link my-account__red" href (click)="removeAccount()">

View File

@ -6,7 +6,6 @@
&__body {
overflow: auto;
height: calc(100%);
// width: calc(100%);
padding-left: 10px;
padding-right: 10px;
font-size: $small-font-size;
@ -14,24 +13,11 @@
outline: 1px dotted greenyellow;
}
&__label {
// text-decoration: underline;
font-size: $small-font-size;
margin-top: 10px;
margin-left: 5px;
color: $font-color-secondary;
}
&__link {
text-decoration: none;
display: block; // width: calc(100% - 20px);
width: 100%; // height: 30px;
padding: 5px 10px; // border: solid 1px black;
&:not(:last-child) {
margin-bottom: 5px;
}
}
&__margin-top {
margin-top: 25px;
}
&__blue {
background-color: $color-primary;
color: #fff;
@ -47,4 +33,26 @@
background-color: lighten($red-button-color, 15);
}
}
&__link {
text-decoration: none;
display: block; // width: calc(100% - 20px);
width: 100%; // height: 30px;
padding: 5px 10px; // border: solid 1px black;
&:not(:last-child) {
margin-bottom: 5px;
}
&--icon {
float: right;
}
&--disabled {
cursor: default;
background-color: darken($color-primary, 4);
&:hover {
background-color: darken($color-primary, 4);
}
}
}
&__margin-top {
margin-top: 25px;
}
}

View File

@ -1,5 +1,7 @@
import { Component, OnInit, Input } from '@angular/core';
import { Store } from '@ngxs/store';
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { Store, Select } from '@ngxs/store';
import { faCheckSquare } from "@fortawesome/free-regular-svg-icons";
import { NotificationService } from '../../../../services/notification.service';
import { StreamElement, StreamTypeEnum, AddStream, RemoveAllStreams } from '../../../../states/streams.state';
@ -12,11 +14,24 @@ import { NavigationService } from '../../../../services/navigation.service';
templateUrl: './my-account.component.html',
styleUrls: ['./my-account.component.scss']
})
export class MyAccountComponent implements OnInit {
export class MyAccountComponent implements OnInit, OnDestroy {
faCheckSquare = faCheckSquare;
availableStreams: StreamElement[] = [];
availableStreams: StreamWrapper[] = [];
@Input() account: AccountWrapper;
private _account: AccountWrapper;
@Input('account')
set account(acc: AccountWrapper) {
this._account = acc;
this.loadStreams(acc);
}
get account(): AccountWrapper {
return this._account;
}
@Select(state => state.streamsstatemodel.streams) streamElements$: Observable<StreamElement[]>;
private streamChangedSub: Subscription;
constructor(
private readonly store: Store,
@ -24,19 +39,41 @@ export class MyAccountComponent implements OnInit {
private notificationService: NotificationService) { }
ngOnInit() {
const instance = this.account.info.instance;
this.availableStreams.length = 0;
this.availableStreams.push(new StreamElement(StreamTypeEnum.global, 'Federated Timeline', this.account.info.id, null, null, instance));
this.availableStreams.push(new StreamElement(StreamTypeEnum.local, 'Local Timeline', this.account.info.id, null, null, instance));
this.availableStreams.push(new StreamElement(StreamTypeEnum.personnal, 'Home', this.account.info.id, null, null, instance));
this.streamChangedSub = this.streamElements$.subscribe((streams: StreamElement[]) => {
this.loadStreams(this.account);
});
}
addStream(stream: StreamElement): boolean {
if (stream) {
ngOnDestroy(): void {
if(this.streamChangedSub) {
this.streamChangedSub.unsubscribe();
}
}
private loadStreams(account: AccountWrapper){
const instance = account.info.instance;
this.availableStreams.length = 0;
this.availableStreams.push(new StreamWrapper(new StreamElement(StreamTypeEnum.global, 'Federated Timeline', account.info.id, null, null, instance)));
this.availableStreams.push(new StreamWrapper(new StreamElement(StreamTypeEnum.local, 'Local Timeline', account.info.id, null, null, instance)));
this.availableStreams.push(new StreamWrapper(new StreamElement(StreamTypeEnum.personnal, 'Home', account.info.id, null, null, instance)));
const loadedStreams = <StreamElement[]>this.store.snapshot().streamsstatemodel.streams;
this.availableStreams.forEach(s => {
if(loadedStreams.find(x => x.id === s.id)){
s.isAdded = true;
} else {
s.isAdded = false;
}
});
}
addStream(stream: StreamWrapper): boolean {
if (stream && !stream.isAdded) {
this.store.dispatch([new AddStream(stream)]).toPromise()
.then(() => {
this.notificationService.notify(`stream added`, false);
});
stream.isAdded = true;
//this.notificationService.notify(`stream added`, false);
});
}
return false;
}
@ -48,3 +85,11 @@ export class MyAccountComponent implements OnInit {
return false;
}
}
class StreamWrapper extends StreamElement {
constructor(stream: StreamElement) {
super(stream.type, stream.name, stream.accountId, stream.tag, stream.list, stream.instance);
}
isAdded: boolean;
}

View File

@ -1,9 +1,9 @@
<div class="left-bar">
<a class="left-bar-button left-bar-button--status left-bar-link" href title="write new message" (click)="createNewStatus()" *ngIf="hasAccounts">
<a class="left-bar-button left-bar-button--status left-bar-link" href title="write new message" (click)="createNewStatus()" (contextmenu)="createNewStatus()" *ngIf="hasAccounts">
<fa-icon [icon]="faCommentAlt"></fa-icon>
<!-- <ion-icon name="md-send"></ion-icon> -->
</a>
<a class="left-bar-button left-bar-button--search left-bar-link" href title="search" (click)="openSearch()" *ngIf="hasAccounts">
<a class="left-bar-button left-bar-button--search left-bar-link" href title="search" (click)="openSearch()" (contextmenu)="openSearch()" *ngIf="hasAccounts">
<ion-icon name="md-search"></ion-icon>
</a>
@ -12,11 +12,11 @@
</app-account-icon>
</div>
<a class="left-bar-button left-bar-button--add left-bar-link" [ngClass]="{'no-accounts': hasAccounts === false }" href title="add new account" (click)="addNewAccount()">
<a class="left-bar-button left-bar-button--add left-bar-link" [ngClass]="{'no-accounts': hasAccounts === false }" href title="add new account" (click)="addNewAccount()" (contextmenu)="addNewAccount()">
<ion-icon name="md-add"></ion-icon>
</a>
<a class="left-bar-button left-bar-button--cog left-bar-link" href title="settings" (click)="openSettings()" *ngIf="hasAccounts">
<a class="left-bar-button left-bar-button--cog left-bar-link" href title="settings" (click)="openSettings()" (contextmenu)="openSettings()" *ngIf="hasAccounts">
<ion-icon name="md-cog"></ion-icon>
</a>
</div>

View File

@ -4,7 +4,9 @@
</a>
<ion-icon *ngIf="isLocked" class="action-bar__lock" name="lock" title="Account can't access this post"></ion-icon>
<a *ngIf="!(isBoostLocked || isLocked)" href class="action-bar__link" title="Boost" [class.boosted]="isBoosted"
<a *ngIf="!(isBoostLocked || isLocked)" href class="action-bar__link" title="Boost"
[class.boosted]="isBoosted"
[class.boosting]="boostIsLoading"
(click)="boost()">
<ion-icon name="md-swap"></ion-icon>
</a>
@ -12,7 +14,9 @@
title="This post cannot be boosted"></ion-icon>
<ion-icon *ngIf="isLocked" class="action-bar__lock" name="lock" title="Account can't access this post"></ion-icon>
<a *ngIf="!isLocked" href class="action-bar__link" title="Favourite" [class.favorited]="isFavorited"
<a *ngIf="!isLocked" href class="action-bar__link" title="Favourite"
[class.favorited]="isFavorited"
[class.favoriting]="favoriteIsLoading"
(click)="favorite()">
<ion-icon name="md-star"></ion-icon>
</a>

View File

@ -1,4 +1,5 @@
@import "variables";
.action-bar {
// outline: 1px solid greenyellow; // height: 20px;
margin: 5px 10px 5px $avatar-column-space;
@ -9,9 +10,11 @@
&__link {
color: $status-secondary-color;
&:hover {
color: $status-links-color;
}
&:not(:last-child) {
margin-right: 15px;
}
@ -50,9 +53,9 @@
.boosted {
color: $boost-color;
&:hover {
color: darken($boost-color, 10);
color: darken($boost-color, 10);
}
}
@ -62,4 +65,70 @@
&:hover {
color: darken($favorite-color, 10);
}
}
@keyframes loadingAnimation {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@-o-keyframes loadingAnimation {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@-moz-keyframes loadingAnimation {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@-webkit-keyframes loadingAnimation {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.boosting,
.favoriting {
-webkit-animation: loadingAnimation 1s infinite;
-moz-animation: loadingAnimation 1s infinite;
-o-animation: loadingAnimation 1s infinite;
animation: loadingAnimation 1s infinite;
}

View File

@ -34,6 +34,9 @@ export class ActionBarComponent implements OnInit, OnDestroy {
isBoostLocked: boolean;
isLocked: boolean;
favoriteIsLoading: boolean;
boostIsLoading: boolean;
isContentWarningActive: boolean = false;
private isProviderSelected: boolean;
@ -48,7 +51,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
constructor(
private readonly store: Store,
private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonService,
private readonly mastodonService: MastodonService,
private readonly notificationService: NotificationService) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
@ -57,8 +60,14 @@ export class ActionBarComponent implements OnInit, OnDestroy {
ngOnInit() {
const status = this.statusWrapper.status;
const account = this.statusWrapper.provider;
this.favoriteStatePerAccountId[account.id] = status.favourited;
this.bootedStatePerAccountId[account.id] = status.reblogged;
if(status.reblog){
this.favoriteStatePerAccountId[account.id] = status.reblog.favourited;
this.bootedStatePerAccountId[account.id] = status.reblog.reblogged;
} else {
this.favoriteStatePerAccountId[account.id] = status.favourited;
this.bootedStatePerAccountId[account.id] = status.reblogged;
}
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
this.checkStatus(accounts);
@ -87,7 +96,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
this.isLocked = false;
}
if(status.sensitive || status.spoiler_text){
if (status.sensitive || status.spoiler_text) {
this.isContentWarningActive = true;
}
@ -113,56 +122,63 @@ export class ActionBarComponent implements OnInit, OnDestroy {
}
boost(): boolean {
//TODO get rid of that
this.selectedAccounts.forEach((account: AccountInfo) => {
if(this.boostIsLoading) return;
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
usableStatus
.then((status: Status) => {
if (this.isBoosted && status.reblogged) {
return this.mastodonService.unreblog(account, status);
} else if(!this.isBoosted && !status.reblogged){
return this.mastodonService.reblog(account, status);
} else {
return Promise.resolve(status);
}
})
.then((boostedStatus: Status) => {
this.bootedStatePerAccountId[account.id] = boostedStatus.reblogged;
this.checkIfBoosted();
// this.isBoosted = !this.isBoosted;
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
});
this.boostIsLoading = true;
const account = this.toolsService.getSelectedAccounts()[0];
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
usableStatus
.then((status: Status) => {
if (this.isBoosted && status.reblogged) {
return this.mastodonService.unreblog(account, status);
} else if (!this.isBoosted && !status.reblogged) {
return this.mastodonService.reblog(account, status);
} else {
return Promise.resolve(status);
}
})
.then((boostedStatus: Status) => {
this.bootedStatePerAccountId[account.id] = boostedStatus.reblogged;
this.checkIfBoosted();
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
})
.then(() => {
this.boostIsLoading = false;
});
return false;
}
favorite(): boolean {
this.selectedAccounts.forEach((account: AccountInfo) => {
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
usableStatus
.then((status: Status) => {
if (this.isFavorited && status.favourited) {
return this.mastodonService.unfavorite(account, status);
} else if(!this.isFavorited && !status.favourited) {
return this.mastodonService.favorite(account, status);
} else {
return Promise.resolve(status);
}
})
.then((favoritedStatus: Status) => {
this.favoriteStatePerAccountId[account.id] = favoritedStatus.favourited;
this.checkIfFavorited();
// this.isFavorited = !this.isFavorited;
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
});
if(this.favoriteIsLoading) return;
this.favoriteIsLoading = true;
const account = this.toolsService.getSelectedAccounts()[0];
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
usableStatus
.then((status: Status) => {
if (this.isFavorited && status.favourited) {
return this.mastodonService.unfavorite(account, status);
} else if (!this.isFavorited && !status.favourited) {
return this.mastodonService.favorite(account, status);
} else {
return Promise.resolve(status);
}
})
.then((favoritedStatus: Status) => {
this.favoriteStatePerAccountId[account.id] = favoritedStatus.favourited;
this.checkIfFavorited();
// this.isFavorited = !this.isFavorited;
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
})
.then(() => {
this.favoriteIsLoading = false;
});
return false;
}
@ -189,9 +205,4 @@ export class ActionBarComponent implements OnInit, OnDestroy {
console.warn('more'); //TODO
return false;
}
// private getSelectedAccounts(): AccountInfo[] {
// var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
// return regAccounts;
// }
}

View File

@ -1,13 +1,12 @@
<div class="profile flexcroll">
<div class="profile flexcroll" #statusstream (scroll)="onScroll()">
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
<div *ngIf="account" class="profile-header" [ngStyle]="{'background-image':'url('+account.header+')'}">
<div *ngIf="displayedAccount" class="profile-header" [ngStyle]="{'background-image':'url('+displayedAccount.header+')'}">
<div class="profile-header__inner">
<!-- <img class="profile-header__header" src="{{account.header}}" alt="header" /> -->
<img class="profile-header__avatar" src="{{account.avatar}}" alt="header" />
<h2 class="profile-header__display-name">{{account.display_name}}</h2>
<h2 class="profile-header__fullhandle"><a href="{{account.url}}" target="_blank">@{{account.acct}}</a></h2>
<img class="profile-header__avatar" src="{{displayedAccount.avatar}}" alt="header" />
<h2 class="profile-header__display-name">{{displayedAccount.display_name}}</h2>
<h2 class="profile-header__fullhandle"><a href="{{displayedAccount.url}}" target="_blank">@{{displayedAccount.acct}}</a></h2>
<div class="profile-header__follow" *ngIf="relationship">
<button class="profile-header__follow--button profile-header__follow--unfollowed" title="follow"
@ -32,14 +31,13 @@
</div>
</div>
<div class="profile-sub-header ">
<div *ngIf="account && hasNote" class="profile-description">
<!-- <div *ngIf="account && account.note" class="profile-description"> -->
<app-databinded-text class="profile-description__content" [textIsSelectable]="false" [text]="account.note"
<div *ngIf="displayedAccount && hasNote" class="profile-description">
<app-databinded-text class="profile-description__content" [textIsSelectable]="false" [text]="displayedAccount.note"
(accountSelected)="browseAccount($event)" (hashtagSelected)="browseHashtag($event)">
</app-databinded-text>
</div>
<div class="profile-fields" *ngIf="account && account.fields.length > 0">
<div class="profile-fields__field" *ngFor="let field of account.fields">
<div class="profile-fields" *ngIf="displayedAccount && displayedAccount.fields.length > 0">
<div class="profile-fields__field" *ngFor="let field of displayedAccount.fields">
<div class="profile-fields__field--value" innerHTML="{{field.value}}" [ngClass]="{'profile-fields__field--validated': field.verified_at }">
</div>
<div class="profile-fields__field--name">
@ -49,8 +47,6 @@
</div>
<div class="profile-statuses">
<app-waiting-animation *ngIf="statusLoading" class="waiting-icon"></app-waiting-animation>
<div *ngIf="!isLoading && !statusLoading && statuses.length == 0" class="profile-no-toots">
no toots found
</div>
@ -60,6 +56,9 @@
(browseAccountEvent)="browseAccount($event)" (browseThreadEvent)="browseThread($event)">
</app-status>
</div>
<app-waiting-animation *ngIf="statusLoading" class="waiting-icon"></app-waiting-animation>
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { faUser, faHourglassHalf, faUserCheck } from "@fortawesome/free-solid-svg-icons";
import { faUser as faUserRegular } from "@fortawesome/free-regular-svg-icons";
@ -25,10 +25,13 @@ export class UserProfileComponent implements OnInit {
faHourglassHalf = faHourglassHalf;
faUserCheck = faUserCheck;
account: Account;
displayedAccount: Account;
hasNote: boolean;
isLoading: boolean;
private maxReached = false;
private maxId: string;
statusLoading: boolean;
error: string;
@ -41,6 +44,8 @@ export class UserProfileComponent implements OnInit {
private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription;
@ViewChild('statusstream') public statustream: ElementRef;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@ -61,12 +66,12 @@ export class UserProfileComponent implements OnInit {
ngOnInit() {
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
if (this.account) {
this.currentlyUsedAccount = accounts.filter(x => x.isSelected)[0];
if (this.displayedAccount) {
const userAccount = accounts.filter(x => x.isSelected)[0];
this.toolsService.findAccount(this.currentlyUsedAccount, this.lastAccountName)
this.toolsService.findAccount(userAccount, this.lastAccountName)
.then((account: Account) => {
this.getFollowStatus(this.currentlyUsedAccount, account);
this.getFollowStatus(userAccount, account);
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
@ -82,7 +87,7 @@ export class UserProfileComponent implements OnInit {
private load(accountName: string) {
this.statuses.length = 0;
this.account = null;
this.displayedAccount = null;
this.isLoading = true;
this.lastAccountName = accountName;
@ -93,11 +98,11 @@ export class UserProfileComponent implements OnInit {
this.isLoading = false;
this.statusLoading = true;
this.account = account;
this.displayedAccount = account;
this.hasNote = account && account.note && account.note !== '<p></p>';
const getFollowStatusPromise = this.getFollowStatus(this.currentlyUsedAccount, this.account);
const getStatusesPromise = this.getStatuses(this.currentlyUsedAccount, this.account);
const getFollowStatusPromise = this.getFollowStatus(this.currentlyUsedAccount, this.displayedAccount);
const getStatusesPromise = this.getStatuses(this.currentlyUsedAccount, this.displayedAccount);
return Promise.all([getFollowStatusPromise, getStatusesPromise]);
})
@ -113,11 +118,25 @@ export class UserProfileComponent implements OnInit {
private getStatuses(userAccount: AccountInfo, account: Account): Promise<void> {
this.statusLoading = true;
return this.mastodonService.getAccountStatuses(userAccount, account.id, false, false, true, null, null, 40)
.then((result: Status[]) => {
for (const status of result) {
const wrapper = new StatusWrapper(status, userAccount);
this.statuses.push(wrapper);
}
.then((statuses: Status[]) => {
this.loadStatus(userAccount, statuses);
// if (statuses.length === 0) {
// this.maxReached = true;
// return;
// }
// for (const status of statuses) {
// const wrapper = new StatusWrapper(status, userAccount);
// this.statuses.push(wrapper);
// }
// this.maxId = this.statuses[this.statuses.length - 1].status.id;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
})
.then(() => {
this.statusLoading = false;
});
}
@ -147,10 +166,10 @@ export class UserProfileComponent implements OnInit {
}
follow(): boolean {
this.currentlyUsedAccount = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(this.currentlyUsedAccount, this.lastAccountName)
const userAccount = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(userAccount, this.lastAccountName)
.then((account: Account) => {
return this.mastodonService.follow(this.currentlyUsedAccount, account);
return this.mastodonService.follow(userAccount, account);
})
.then((relationship: Relationship) => {
this.relationship = relationship;
@ -162,10 +181,10 @@ export class UserProfileComponent implements OnInit {
}
unfollow(): boolean {
this.currentlyUsedAccount = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(this.currentlyUsedAccount, this.lastAccountName)
const userAccount = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(userAccount, this.lastAccountName)
.then((account: Account) => {
return this.mastodonService.unfollow(this.currentlyUsedAccount, account);
return this.mastodonService.unfollow(userAccount, account);
})
.then((relationship: Relationship) => {
this.relationship = relationship;
@ -175,4 +194,44 @@ export class UserProfileComponent implements OnInit {
});
return false;
}
onScroll() {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
if (atBottom) {
this.scrolledToBottom();
}
}
private scrolledToBottom() {
if (this.statusLoading || this.maxReached) return;
this.statusLoading = true;
const userAccount = this.currentlyUsedAccount;
this.mastodonService.getAccountStatuses(userAccount, this.displayedAccount.id, false, false, true, this.maxId, null, 40)
.then((statuses: Status[]) => {
this.loadStatus(userAccount, statuses);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
})
.then(() => {
this.statusLoading = false;
});
}
private loadStatus(userAccount: AccountInfo, statuses: Status[]){
if (statuses.length === 0) {
this.maxReached = true;
return;
}
for (const status of statuses) {
const wrapper = new StatusWrapper(status, userAccount);
this.statuses.push(wrapper);
}
this.maxId = this.statuses[this.statuses.length - 1].status.id;
}
}

View File

@ -29,6 +29,9 @@ export class MediaService {
let medias = this.mediaSubject.value;
medias.push(wrapper);
if(medias.length > 4){
medias.splice(0, 1);
}
this.mediaSubject.next(medias);
this.mastodonService.uploadMediaAttachment(account, file, null)

View File

@ -157,7 +157,7 @@ export interface Status {
favourited: boolean;
sensitive: boolean;
spoiler_text: string;
visibility: string;
visibility: 'public' | 'unlisted' | 'private' | 'direct';
media_attachments: Attachment[];
mentions: Mention[];
tags: Tag[];

View File

@ -44,10 +44,10 @@ export class ToolsService {
.then((result: Results) => {
if(accountName[0] === '@') accountName = accountName.substr(1);
const foundAccount = result.accounts.filter(
const foundAccount = result.accounts.find(
x => x.acct.toLowerCase() === accountName.toLowerCase()
|| x.acct.toLowerCase() === accountName.toLowerCase().split('@')[0]
)[0];
|| x.acct.toLowerCase().split('@')[0] === accountName.toLowerCase().split('@')[0]
);
return foundAccount;
});
}