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 /release
# compiled output # compiled output
/release
/dist /dist
/dist-server /dist-server
/tmp /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() { function createWindow() {
// Create the browser window. // 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); var server = http.createServer(requestHandler).listen(9527);
win.loadURL('http://localhost:9527'); win.loadURL('http://localhost:9527');

1114
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,17 @@
{ {
"name": "sengi", "name": "sengi",
"version": "0.0.0", "version": "0.6.0",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"main": "main-electron.js", "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": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
@ -12,7 +21,8 @@
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e", "e2e": "ng e2e",
"electron": "ng build --prod && electron .", "electron": "ng build --prod && electron .",
"electron-debug": "ng build && electron ." "electron-debug": "ng build && electron .",
"dist": "npm run build && build --publish onTagOrDraft"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
@ -34,7 +44,6 @@
"@ngxs/store": "^3.2.0", "@ngxs/store": "^3.2.0",
"bootstrap": "^4.1.3", "bootstrap": "^4.1.3",
"core-js": "^2.5.4", "core-js": "^2.5.4",
"electron": "^4.0.6",
"ionicons": "^4.4.3", "ionicons": "^4.4.3",
"rxjs": "^6.4.0", "rxjs": "^6.4.0",
"tslib": "^1.9.0", "tslib": "^1.9.0",
@ -49,6 +58,8 @@
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4", "@types/node": "~8.9.4",
"codelyzer": "~4.2.1", "codelyzer": "~4.2.1",
"electron": "^4.0.6",
"electron-builder": "^20.39.0",
"jasmine-core": "~2.99.1", "jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1", "jasmine-spec-reporter": "~4.2.1",
"karma": "~1.7.1", "karma": "~1.7.1",
@ -61,5 +72,57 @@
"ts-node": "~5.0.1", "ts-node": "~5.0.1",
"tslint": "~5.9.1", "tslint": "~5.9.1",
"typescript": "~3.2.4" "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 this.dragoverSub = this.dragoverSubject
.pipe( .pipe(
debounceTime(150) debounceTime(1500)
) )
.subscribe(() => { .subscribe(() => {
console.warn('disable drag');
this.drag = false; this.drag = false;
}) })
} }

View File

@ -34,8 +34,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private maxCharLength: number; private maxCharLength: number;
charCountLeft: number; charCountLeft: number;
postCounts: number = 1; postCounts: number = 1;
isSending: boolean; isSending: boolean;
mentionTooFarAwayError: boolean;
@Input() statusReplyingToWrapper: StatusWrapper; @Input() statusReplyingToWrapper: StatusWrapper;
@Output() onClose = new EventEmitter(); @Output() onClose = new EventEmitter();
@ -76,6 +76,21 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.status += `@${mention} `; 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; this.title = this.statusReplyingTo.spoiler_text;
} }
@ -100,8 +115,19 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.notificationService.notifyHttpError(err); this.notificationService.notifyHttpError(err);
}); });
if (!this.statusReplyingToWrapper) {
this.instancesInfoService.getDefaultPrivacy(selectedAccount) this.instancesInfoService.getDefaultPrivacy(selectedAccount)
.then((defaultPrivacy: VisibilityEnum) => { .then((defaultPrivacy: VisibilityEnum) => {
this.setVisibility(defaultPrivacy);
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
}
}
}
private setVisibility(defaultPrivacy: VisibilityEnum) {
switch (defaultPrivacy) { switch (defaultPrivacy) {
case VisibilityEnum.Public: case VisibilityEnum.Public:
this.selectedPrivacy = 'Public'; this.selectedPrivacy = 'Public';
@ -116,14 +142,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.selectedPrivacy = 'DM'; this.selectedPrivacy = 'DM';
break; break;
} }
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
} }
}
mentionTooFarAwayError: boolean;
private countStatusChar(status: string) { private countStatusChar(status: string) {
this.mentionTooFarAwayError = false; this.mentionTooFarAwayError = false;
@ -144,16 +163,6 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.postCounts = 1; this.postCounts = 1;
return; 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]; const currentStatus = parseStatus[parseStatus.length - 1];
@ -182,6 +191,9 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
globalUniqueMentions.push(mention); globalUniqueMentions.push(mention);
} }
const selectedUser = this.toolsService.getSelectedAccounts()[0];
globalUniqueMentions = globalUniqueMentions.filter(x => x.toLowerCase() !== `${selectedUser.username}@${selectedUser.instance}`.toLowerCase());
return globalUniqueMentions; return globalUniqueMentions;
} }
@ -196,7 +208,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.isSending = true; this.isSending = true;
let visibility: VisibilityEnum = VisibilityEnum.Unknown; let visibility: VisibilityEnum = VisibilityEnum.Unknown;
switch (this.selectedPrivacy) { //FIXME: in case of responding, set the visibility to original switch (this.selectedPrivacy) {
case 'Public': case 'Public':
visibility = VisibilityEnum.Public; visibility = VisibilityEnum.Public;
break; break;

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

View File

@ -1,7 +1,8 @@
<div class="my-account__body flexcroll"> <div class="my-account__body flexcroll">
<h4 class="my-account__label">add column:</h4> <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)"> <a class="my-account__link my-account__blue" href *ngFor="let stream of availableStreams"
{{ stream.name }} (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> </a>
<h4 class="my-account__label my-account__margin-top">remove account from sengi:</h4> <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()"> <a class="my-account__link my-account__red" href (click)="removeAccount()">

View File

@ -6,7 +6,6 @@
&__body { &__body {
overflow: auto; overflow: auto;
height: calc(100%); height: calc(100%);
// width: calc(100%);
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
font-size: $small-font-size; font-size: $small-font-size;
@ -14,24 +13,11 @@
outline: 1px dotted greenyellow; outline: 1px dotted greenyellow;
} }
&__label { &__label {
// text-decoration: underline;
font-size: $small-font-size; font-size: $small-font-size;
margin-top: 10px; margin-top: 10px;
margin-left: 5px; margin-left: 5px;
color: $font-color-secondary; 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 { &__blue {
background-color: $color-primary; background-color: $color-primary;
color: #fff; color: #fff;
@ -47,4 +33,26 @@
background-color: lighten($red-button-color, 15); 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 { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { Store } from '@ngxs/store'; 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 { NotificationService } from '../../../../services/notification.service';
import { StreamElement, StreamTypeEnum, AddStream, RemoveAllStreams } from '../../../../states/streams.state'; import { StreamElement, StreamTypeEnum, AddStream, RemoveAllStreams } from '../../../../states/streams.state';
@ -12,11 +14,24 @@ import { NavigationService } from '../../../../services/navigation.service';
templateUrl: './my-account.component.html', templateUrl: './my-account.component.html',
styleUrls: ['./my-account.component.scss'] styleUrls: ['./my-account.component.scss']
}) })
export class MyAccountComponent implements OnInit { export class MyAccountComponent implements OnInit, OnDestroy {
availableStreams: StreamElement[] = []; faCheckSquare = faCheckSquare;
@Input() account: AccountWrapper; availableStreams: StreamWrapper[] = [];
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( constructor(
private readonly store: Store, private readonly store: Store,
@ -24,18 +39,40 @@ export class MyAccountComponent implements OnInit {
private notificationService: NotificationService) { } private notificationService: NotificationService) { }
ngOnInit() { ngOnInit() {
const instance = this.account.info.instance; this.streamChangedSub = this.streamElements$.subscribe((streams: StreamElement[]) => {
this.availableStreams.length = 0; this.loadStreams(this.account);
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));
} }
addStream(stream: StreamElement): boolean { ngOnDestroy(): void {
if (stream) { 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() this.store.dispatch([new AddStream(stream)]).toPromise()
.then(() => { .then(() => {
this.notificationService.notify(`stream added`, false); stream.isAdded = true;
//this.notificationService.notify(`stream added`, false);
}); });
} }
return false; return false;
@ -48,3 +85,11 @@ export class MyAccountComponent implements OnInit {
return false; 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"> <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> <fa-icon [icon]="faCommentAlt"></fa-icon>
<!-- <ion-icon name="md-send"></ion-icon> --> <!-- <ion-icon name="md-send"></ion-icon> -->
</a> </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> <ion-icon name="md-search"></ion-icon>
</a> </a>
@ -12,11 +12,11 @@
</app-account-icon> </app-account-icon>
</div> </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> <ion-icon name="md-add"></ion-icon>
</a> </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> <ion-icon name="md-cog"></ion-icon>
</a> </a>
</div> </div>

View File

@ -4,7 +4,9 @@
</a> </a>
<ion-icon *ngIf="isLocked" class="action-bar__lock" name="lock" title="Account can't access this post"></ion-icon> <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()"> (click)="boost()">
<ion-icon name="md-swap"></ion-icon> <ion-icon name="md-swap"></ion-icon>
</a> </a>
@ -12,7 +14,9 @@
title="This post cannot be boosted"></ion-icon> 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> <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()"> (click)="favorite()">
<ion-icon name="md-star"></ion-icon> <ion-icon name="md-star"></ion-icon>
</a> </a>

View File

@ -1,4 +1,5 @@
@import "variables"; @import "variables";
.action-bar { .action-bar {
// outline: 1px solid greenyellow; // height: 20px; // outline: 1px solid greenyellow; // height: 20px;
margin: 5px 10px 5px $avatar-column-space; margin: 5px 10px 5px $avatar-column-space;
@ -9,9 +10,11 @@
&__link { &__link {
color: $status-secondary-color; color: $status-secondary-color;
&:hover { &:hover {
color: $status-links-color; color: $status-links-color;
} }
&:not(:last-child) { &:not(:last-child) {
margin-right: 15px; margin-right: 15px;
} }
@ -63,3 +66,69 @@
color: darken($favorite-color, 10); 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; isBoostLocked: boolean;
isLocked: boolean; isLocked: boolean;
favoriteIsLoading: boolean;
boostIsLoading: boolean;
isContentWarningActive: boolean = false; isContentWarningActive: boolean = false;
private isProviderSelected: boolean; private isProviderSelected: boolean;
@ -57,8 +60,14 @@ export class ActionBarComponent implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
const status = this.statusWrapper.status; const status = this.statusWrapper.status;
const account = this.statusWrapper.provider; const account = this.statusWrapper.provider;
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.favoriteStatePerAccountId[account.id] = status.favourited;
this.bootedStatePerAccountId[account.id] = status.reblogged; this.bootedStatePerAccountId[account.id] = status.reblogged;
}
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => { this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
this.checkStatus(accounts); this.checkStatus(accounts);
@ -113,9 +122,10 @@ export class ActionBarComponent implements OnInit, OnDestroy {
} }
boost(): boolean { boost(): boolean {
//TODO get rid of that if(this.boostIsLoading) return;
this.selectedAccounts.forEach((account: AccountInfo) => {
this.boostIsLoading = true;
const account = this.toolsService.getSelectedAccounts()[0];
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper); const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
usableStatus usableStatus
.then((status: Status) => { .then((status: Status) => {
@ -130,19 +140,22 @@ export class ActionBarComponent implements OnInit, OnDestroy {
.then((boostedStatus: Status) => { .then((boostedStatus: Status) => {
this.bootedStatePerAccountId[account.id] = boostedStatus.reblogged; this.bootedStatePerAccountId[account.id] = boostedStatus.reblogged;
this.checkIfBoosted(); this.checkIfBoosted();
// this.isBoosted = !this.isBoosted;
}) })
.catch((err: HttpErrorResponse) => { .catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err); this.notificationService.notifyHttpError(err);
}); })
.then(() => {
this.boostIsLoading = false;
}); });
return false; return false;
} }
favorite(): boolean { favorite(): boolean {
this.selectedAccounts.forEach((account: AccountInfo) => { if(this.favoriteIsLoading) return;
this.favoriteIsLoading = true;
const account = this.toolsService.getSelectedAccounts()[0];
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper); const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
usableStatus usableStatus
.then((status: Status) => { .then((status: Status) => {
@ -161,8 +174,11 @@ export class ActionBarComponent implements OnInit, OnDestroy {
}) })
.catch((err: HttpErrorResponse) => { .catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err); this.notificationService.notifyHttpError(err);
})
.then(() => {
this.favoriteIsLoading = false;
}); });
});
return false; return false;
} }
@ -189,9 +205,4 @@ export class ActionBarComponent implements OnInit, OnDestroy {
console.warn('more'); //TODO console.warn('more'); //TODO
return false; 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> <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"> <div class="profile-header__inner">
<!-- <img class="profile-header__header" src="{{account.header}}" alt="header" /> --> <img class="profile-header__avatar" src="{{displayedAccount.avatar}}" alt="header" />
<img class="profile-header__avatar" src="{{account.avatar}}" alt="header" /> <h2 class="profile-header__display-name">{{displayedAccount.display_name}}</h2>
<h2 class="profile-header__display-name">{{account.display_name}}</h2> <h2 class="profile-header__fullhandle"><a href="{{displayedAccount.url}}" target="_blank">@{{displayedAccount.acct}}</a></h2>
<h2 class="profile-header__fullhandle"><a href="{{account.url}}" target="_blank">@{{account.acct}}</a></h2>
<div class="profile-header__follow" *ngIf="relationship"> <div class="profile-header__follow" *ngIf="relationship">
<button class="profile-header__follow--button profile-header__follow--unfollowed" title="follow" <button class="profile-header__follow--button profile-header__follow--unfollowed" title="follow"
@ -32,14 +31,13 @@
</div> </div>
</div> </div>
<div class="profile-sub-header "> <div class="profile-sub-header ">
<div *ngIf="account && hasNote" class="profile-description"> <div *ngIf="displayedAccount && hasNote" class="profile-description">
<!-- <div *ngIf="account && account.note" class="profile-description"> --> <app-databinded-text class="profile-description__content" [textIsSelectable]="false" [text]="displayedAccount.note"
<app-databinded-text class="profile-description__content" [textIsSelectable]="false" [text]="account.note"
(accountSelected)="browseAccount($event)" (hashtagSelected)="browseHashtag($event)"> (accountSelected)="browseAccount($event)" (hashtagSelected)="browseHashtag($event)">
</app-databinded-text> </app-databinded-text>
</div> </div>
<div class="profile-fields" *ngIf="account && account.fields.length > 0"> <div class="profile-fields" *ngIf="displayedAccount && displayedAccount.fields.length > 0">
<div class="profile-fields__field" *ngFor="let field of account.fields"> <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 class="profile-fields__field--value" innerHTML="{{field.value}}" [ngClass]="{'profile-fields__field--validated': field.verified_at }">
</div> </div>
<div class="profile-fields__field--name"> <div class="profile-fields__field--name">
@ -49,8 +47,6 @@
</div> </div>
<div class="profile-statuses"> <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"> <div *ngIf="!isLoading && !statusLoading && statuses.length == 0" class="profile-no-toots">
no toots found no toots found
</div> </div>
@ -60,6 +56,9 @@
(browseAccountEvent)="browseAccount($event)" (browseThreadEvent)="browseThread($event)"> (browseAccountEvent)="browseAccount($event)" (browseThreadEvent)="browseThread($event)">
</app-status> </app-status>
</div> </div>
<app-waiting-animation *ngIf="statusLoading" class="waiting-icon"></app-waiting-animation>
</div> </div>
</div> </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 { HttpErrorResponse } from '@angular/common/http';
import { faUser, faHourglassHalf, faUserCheck } from "@fortawesome/free-solid-svg-icons"; import { faUser, faHourglassHalf, faUserCheck } from "@fortawesome/free-solid-svg-icons";
import { faUser as faUserRegular } from "@fortawesome/free-regular-svg-icons"; import { faUser as faUserRegular } from "@fortawesome/free-regular-svg-icons";
@ -25,10 +25,13 @@ export class UserProfileComponent implements OnInit {
faHourglassHalf = faHourglassHalf; faHourglassHalf = faHourglassHalf;
faUserCheck = faUserCheck; faUserCheck = faUserCheck;
account: Account; displayedAccount: Account;
hasNote: boolean; hasNote: boolean;
isLoading: boolean; isLoading: boolean;
private maxReached = false;
private maxId: string;
statusLoading: boolean; statusLoading: boolean;
error: string; error: string;
@ -41,6 +44,8 @@ export class UserProfileComponent implements OnInit {
private accounts$: Observable<AccountInfo[]>; private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription; private accountSub: Subscription;
@ViewChild('statusstream') public statustream: ElementRef;
@Output() browseAccountEvent = new EventEmitter<string>(); @Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>(); @Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>(); @Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@ -61,12 +66,12 @@ export class UserProfileComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => { this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
if (this.account) { if (this.displayedAccount) {
this.currentlyUsedAccount = accounts.filter(x => x.isSelected)[0]; const userAccount = accounts.filter(x => x.isSelected)[0];
this.toolsService.findAccount(this.currentlyUsedAccount, this.lastAccountName) this.toolsService.findAccount(userAccount, this.lastAccountName)
.then((account: Account) => { .then((account: Account) => {
this.getFollowStatus(this.currentlyUsedAccount, account); this.getFollowStatus(userAccount, account);
}) })
.catch((err: HttpErrorResponse) => { .catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err); this.notificationService.notifyHttpError(err);
@ -82,7 +87,7 @@ export class UserProfileComponent implements OnInit {
private load(accountName: string) { private load(accountName: string) {
this.statuses.length = 0; this.statuses.length = 0;
this.account = null; this.displayedAccount = null;
this.isLoading = true; this.isLoading = true;
this.lastAccountName = accountName; this.lastAccountName = accountName;
@ -93,11 +98,11 @@ export class UserProfileComponent implements OnInit {
this.isLoading = false; this.isLoading = false;
this.statusLoading = true; this.statusLoading = true;
this.account = account; this.displayedAccount = account;
this.hasNote = account && account.note && account.note !== '<p></p>'; this.hasNote = account && account.note && account.note !== '<p></p>';
const getFollowStatusPromise = this.getFollowStatus(this.currentlyUsedAccount, this.account); const getFollowStatusPromise = this.getFollowStatus(this.currentlyUsedAccount, this.displayedAccount);
const getStatusesPromise = this.getStatuses(this.currentlyUsedAccount, this.account); const getStatusesPromise = this.getStatuses(this.currentlyUsedAccount, this.displayedAccount);
return Promise.all([getFollowStatusPromise, getStatusesPromise]); return Promise.all([getFollowStatusPromise, getStatusesPromise]);
}) })
@ -113,11 +118,25 @@ export class UserProfileComponent implements OnInit {
private getStatuses(userAccount: AccountInfo, account: Account): Promise<void> { private getStatuses(userAccount: AccountInfo, account: Account): Promise<void> {
this.statusLoading = true; this.statusLoading = true;
return this.mastodonService.getAccountStatuses(userAccount, account.id, false, false, true, null, null, 40) return this.mastodonService.getAccountStatuses(userAccount, account.id, false, false, true, null, null, 40)
.then((result: Status[]) => { .then((statuses: Status[]) => {
for (const status of result) { this.loadStatus(userAccount, statuses);
const wrapper = new StatusWrapper(status, userAccount);
this.statuses.push(wrapper); // 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; this.statusLoading = false;
}); });
} }
@ -147,10 +166,10 @@ export class UserProfileComponent implements OnInit {
} }
follow(): boolean { follow(): boolean {
this.currentlyUsedAccount = this.toolsService.getSelectedAccounts()[0]; const userAccount = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(this.currentlyUsedAccount, this.lastAccountName) this.toolsService.findAccount(userAccount, this.lastAccountName)
.then((account: Account) => { .then((account: Account) => {
return this.mastodonService.follow(this.currentlyUsedAccount, account); return this.mastodonService.follow(userAccount, account);
}) })
.then((relationship: Relationship) => { .then((relationship: Relationship) => {
this.relationship = relationship; this.relationship = relationship;
@ -162,10 +181,10 @@ export class UserProfileComponent implements OnInit {
} }
unfollow(): boolean { unfollow(): boolean {
this.currentlyUsedAccount = this.toolsService.getSelectedAccounts()[0]; const userAccount = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(this.currentlyUsedAccount, this.lastAccountName) this.toolsService.findAccount(userAccount, this.lastAccountName)
.then((account: Account) => { .then((account: Account) => {
return this.mastodonService.unfollow(this.currentlyUsedAccount, account); return this.mastodonService.unfollow(userAccount, account);
}) })
.then((relationship: Relationship) => { .then((relationship: Relationship) => {
this.relationship = relationship; this.relationship = relationship;
@ -175,4 +194,44 @@ export class UserProfileComponent implements OnInit {
}); });
return false; 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; let medias = this.mediaSubject.value;
medias.push(wrapper); medias.push(wrapper);
if(medias.length > 4){
medias.splice(0, 1);
}
this.mediaSubject.next(medias); this.mediaSubject.next(medias);
this.mastodonService.uploadMediaAttachment(account, file, null) this.mastodonService.uploadMediaAttachment(account, file, null)

View File

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

View File

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